Package com.relativitas.maven.plugins.formatter

Source Code of com.relativitas.maven.plugins.formatter.FormatterMojo

package com.relativitas.maven.plugins.formatter;

/*
* Copyright 2010. 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.
*/

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.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.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.
*
* @goal format
* @phase process-sources
*
* @author jecki
* @author Matt Blanchette
*/
public class FormatterMojo extends AbstractMojo {
  private static final String CACHE_PROPERTIES_FILENAME = "maven-java-formatter-cache.properties";
  private static final String[] DEFAULT_INCLUDES = new String[] { "**/*.java" };

  static final String LINE_ENDING_AUTO = "AUTO";
  static final String LINE_ENDING_KEEP = "KEEP";
  static final String LINE_ENDING_LF = "LF";
  static final String LINE_ENDING_CRLF = "CRLF";
  static final String LINE_ENDING_CR = "CR";

  static final String LINE_ENDING_LF_CHAR = "\n";
  static final String LINE_ENDING_CRLF_CHARS = "\r\n";
  static final String LINE_ENDING_CR_CHAR = "\r";

  /**
   * ResourceManager for retrieving the configFile resource.
   *
   * @component
   * @required
   * @readonly
   */
  private ResourceManager resourceManager;

  /**
   * Project's source directory as specified in the POM.
   *
   * @parameter expression="${project.build.sourceDirectory}"
   * @readonly
   * @required
   */
  private File sourceDirectory;

  /**
   * Project's test source directory as specified in the POM.
   *
   * @parameter expression="${project.build.testSourceDirectory}"
   * @readonly
   * @required
   */
  private File testSourceDirectory;

  /**
   * Project's target directory as specified in the POM.
   *
   * @parameter expression="${project.build.directory}"
   * @readonly
   * @required
   */
  private File targetDirectory;

  /**
   * Project's base directory.
   *
   * @parameter expression="${basedir}"
   * @readonly
   * @required
   */
  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.
   *
   * @parameter
   * @since 0.4
   */
  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 <code>**&#47;*.java</code>
   *
   * @parameter
   * @since 0.3
   */
  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.
   *
   * @parameter
   * @since 0.3
   */
  private String[] excludes;
 
  /**
   * Java compiler source version.
   *
   * @parameter default-value="1.5" expression="${maven.compiler.source}"
   */
  private String compilerSource;

  /**
   * Java compiler compliance version.
   *
   * @parameter default-value="1.5" expression="${maven.compiler.source}"
   */
  private String compilerCompliance;

  /**
   * Java compiler target version.
   *
   * @parameter default-value="1.5" expression="${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.
   *
   * @parameter default-value="${project.build.sourceEncoding}"
   * @since 0.3
   */
   private String encoding;
 
  /**
   * Sets the line-ending of files after formatting. Valid values are:
   * <ul>
   * <li><b>"AUTO"</b> - Use line endings of current system</li>
   * <li><b>"KEEP"</b> - Preserve line endings of files, default to AUTO if
   * ambiguous</li>
   * <li><b>"LF"</b> - Use Unix and Mac style line endings</li>
   * <li><b>"CRLF"</b> - Use DOS and Windows style line endings</li>
   * <li><b>"CR"</b> - Use early Mac style line endings</li>
   * </ul>
   *
   * @parameter default-value="AUTO"
   * @since 0.2.0
   */
  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.
   *
   * @parameter default-value="false"
   * @since 0.2.0
   */
  private Boolean overrideConfigCompilerVersion;

  /**
   * Whether the formatting is skipped.
   *
   * @parameter default-value="false" expression="${skipFormat}"
   * @since 0.5
   */
  private Boolean skipFormatting;

  private CodeFormatter formatter;

  private PlexusIoFileResourceCollection collection;
 
  /**
   * @see org.apache.maven.plugin.AbstractMojo#execute()
   */
  public void execute() throws MojoExecutionException {
    if (skipFormatting){
      getLog().info("Formatting is skipped");
      return;
    }
    long startClock = System.currentTimeMillis();

    if (StringUtils.isEmpty(encoding)) {
      encoding = ReaderFactory.FILE_ENCODING;
      getLog().warn(
        "File encoding has not been set, using platform encoding (" + encoding
            + ") to format source files, i.e. build is platform dependent!");
    } else {
      try {
        "Test Encoding".getBytes(encoding);
      }
      catch (UnsupportedEncodingException e) {
        throw new MojoExecutionException("Encoding '" + encoding + "' is not supported");
      }
      getLog().info("Using '" + encoding + "' encoding to format source files.");
    }
   
    if (!LINE_ENDING_AUTO.equals(lineEnding)
        && !LINE_ENDING_KEEP.equals(lineEnding)
        && !LINE_ENDING_LF.equals(lineEnding)
        && !LINE_ENDING_CRLF.equals(lineEnding)
        && !LINE_ENDING_CR.equals(lineEnding)) {
      throw new MojoExecutionException(
          "Unknown value for lineEnding parameter");
    }

    createResourceCollection();
   
    List files = new ArrayList();
    try {
      if( directories != null ) {
        for( File directory : directories ) {
          if( directory.exists() && directory.isDirectory() ) {
            collection.setBaseDir(directory);
            addCollectionFiles(files);
          }
        }
      } else { // Using defaults of source main and test dirs
        if (sourceDirectory != null && sourceDirectory.exists()
            && sourceDirectory.isDirectory()) {
          collection.setBaseDir(sourceDirectory);
          addCollectionFiles(files);
        }
        if (testSourceDirectory != null && testSourceDirectory.exists()
            && testSourceDirectory.isDirectory()) {
          collection.setBaseDir(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 = (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() {
    collection = new PlexusIoFileResourceCollection();
    if ( includes != null && includes.length > 0 ) {
      collection.setIncludes(includes);
    } else {
      collection.setIncludes(DEFAULT_INCLUDES);
    }
    collection.setExcludes(excludes);
    collection.setIncludingEmptyDirectories(false);

    IncludeExcludeFileSelector fileSelector = new IncludeExcludeFileSelector();
    fileSelector.setIncludes(DEFAULT_INCLUDES);
    collection.setFileSelectors(new FileSelector[]{fileSelector});
  }
 
  /**
   * Add source files from the {@link PlexusIoFileResourceCollection} to the files list.
   *
   * @param files
   * @throws IOException
   */
  void addCollectionFiles(List files) throws IOException {
    Iterator resources = collection.getResources();
    while(resources.hasNext()) {
        PlexusIoFileResource resource = (PlexusIoFileResource)resources.next();
        files.add(resource.getFile());
    }
  }
 
  private String getBasedirPath() {
    try {
      return basedir.getCanonicalPath();
    } catch (Exception e) {
      return "";
    }
  }

  private void storeFileHashCache(Properties props) {
    File cacheFile = new File(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);
    }
  }

  private Properties readFileHashCacheFile() {
    Properties props = new Properties();
    Log log = getLog();
    if (!targetDirectory.exists()) {
      targetDirectory.mkdirs();
    } else if (!targetDirectory.isDirectory()) {
      log.warn("Something strange here as the "
          + "supposedly target directory is not a directory.");
      return props;
    }

    File cacheFile = new File(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;
  }

  /**
   * @param file
   * @param rc
   * @param hashCache
   * @param basedirPath
   */
  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
   * @param rc
   * @param hashCache
   * @param basedirPath
   * @throws IOException
   * @throws BadLocationException
   */
  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 = 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++;
  }

  /**
   * @param str
   * @return
   * @throws UnsupportedEncodingException
   */
  private String md5hash(String str) throws UnsupportedEncodingException {
    return DigestUtils.md5Hex(str.getBytes(encoding));
  }

  /**
   * Read the given file and return the content as a string.
   *
   * @param file
   * @return
   * @throws java.io.IOException
   */
  private String readFileAsString(File file) throws java.io.IOException {
    StringBuilder fileData = new StringBuilder(1000);
    BufferedReader reader = null;
    try {
      reader = new BufferedReader(ReaderFactory.newReader(file, 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
   * @param file
   * @throws IOException
   */
  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, encoding));
      bw.write(str);
    } finally {
      IOUtil.close(bw);
    }
  }

  /**
   * Create a {@link CodeFormatter} instance to be used by this mojo.
   *
   * @throws MojoExecutionException
   */
  private void createCodeFormatter() throws MojoExecutionException {
    Map options = getFormattingOptions();
    formatter = ToolFactory.createCodeFormatter(options);
  }

  /**
   * Return the options to be passed when creating {@link CodeFormatter}
   * instance.
   *
   * @return
   * @throws MojoExecutionException
   */
  private Map getFormattingOptions() throws MojoExecutionException {
    Map options = new HashMap();
    options.put(JavaCore.COMPILER_SOURCE, compilerSource);
    options.put(JavaCore.COMPILER_COMPLIANCE, compilerCompliance);
    options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM,
        compilerTargetPlatform);

    if (configFile != null) {
      Map config = getOptionsFromConfigFile();
      if (Boolean.TRUE.equals(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
   * @throws MojoExecutionException
   */
  private Map getOptionsFromConfigFile() throws MojoExecutionException {

    InputStream configInput = null;
    try {
      resourceManager.addSearchPath(FileResourceLoader.ID, basedir
          .getAbsolutePath());
      configInput = resourceManager.getResourceAsInputStream(configFile);
    } catch (ResourceNotFoundException e) {
      throw new MojoExecutionException("Config file [" + configFile
          + "] cannot be found", e);
    }

    if (configInput == null) {
      throw new MojoExecutionException("Config file [" + configFile
          + "] does not exist");
    } else {
      try {
        ConfigReader configReader = new ConfigReader();
        return configReader.read(configInput);
      } catch (IOException e) {
        throw new MojoExecutionException("Cannot read config file ["
            + configFile + "]", e);
      } catch (SAXException e) {
        throw new MojoExecutionException("Cannot parse config file ["
            + configFile + "]", e);
      } catch (ConfigReadException e) {
        throw new MojoExecutionException(e.getMessage(), e);
      } finally {
        if (configInput != null) {
          try {
            configInput.close();
          } catch (IOException e) {
          }
        }
      }
    }
  }

  /**
   * 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.
   *
   * @return
   */
  String getLineEnding(String fileDataString) {
    String lineEnd = null;
    if (LINE_ENDING_KEEP.equals(lineEnding)) {
      lineEnd = determineLineEnding(fileDataString);
    } else if (LINE_ENDING_LF.equals(lineEnding)) {
      lineEnd = LINE_ENDING_LF_CHAR;
    } else if (LINE_ENDING_CRLF.equals(lineEnding)) {
      lineEnd = LINE_ENDING_CRLF_CHARS;
    } else if (LINE_ENDING_CR.equals(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.
   *
   * @return
   */
  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;
  }

  private class ResultCollector {
    private int successCount;
    private int failCount;
    private int skippedCount;
  }
}
TOP

Related Classes of com.relativitas.maven.plugins.formatter.FormatterMojo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.