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

de.plushnikov.doctorjim.ImportProcessor Maven / Gradle / Ivy

There is a newer version: 1.24.1
Show newest version
package de.plushnikov.doctorjim;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseException;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.ThisExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithRange;
import com.github.javaparser.ast.type.ClassOrInterfaceType;

/**
 * Main class for Importbeautifikation
 *
 * @author Plushnikov Michail
 * @version $Id: $
 */
public class ImportProcessor {
  /**
   * Logger of this class
   */
  private final Log sLogger = LogFactory.getLog(ImportProcessor.class);

  private static final String NEW_LINE = System.lineSeparator();

  private static final String DEFAULT_PACKAGE = "";

  private static final String JAVA_LANG_PACKAGE = "java.lang";

  private static final String IMPORT_STATEMENT = "import ";

  private static final String STAR_IMPORT = ".*";

  /**
   * Import parser behavior for .* imports
   */
  private boolean mStrict;

  private boolean removeUnusedImports;

  /**
   * Encoding to read or write
   */
  private String mEncoding;

  /**
   * Adds some common used types from java.lang package
   *
   * @param pTypes Collection to add types to
   */
  private void initializeJavaLang( Collection pTypes ) {
    // add default java lang import
    pTypes.add("java.lang.*");
    pTypes.add("java.lang.Object");
    pTypes.add("java.lang.Short");
    pTypes.add("java.lang.Integer");
    pTypes.add("java.lang.Long");
    pTypes.add("java.lang.Float");
    pTypes.add("java.lang.Double");
    pTypes.add("java.lang.String");
    pTypes.add("java.lang.System");
    pTypes.add("java.lang.Character");
    pTypes.add("java.lang.Boolean");
    pTypes.add("java.lang.Byte");
    pTypes.add("java.lang.Number");
    pTypes.add("java.lang.Exeption");
    pTypes.add("java.lang.StringBuilder");
    pTypes.add("java.lang.StringBuffer");
  }

  /**
   * Apply workaround for import of Map.Entry.
   * 
   * @param pImport2Replace String of import to be replaced
   * @return String of import, which should be replaced
   */
  private String changeReplacementWorkaround( String pImport2Replace ) {
    String result = pImport2Replace;
    if (pImport2Replace.equals("java.util.Map.Entry")) {
      result = "java.util.Map";
    }
    return result;
  }

  /**
   * Default constructor
   */
  public ImportProcessor( ) {
    mStrict = true;
    removeUnusedImports = false;
    mEncoding = null;
    sLogger.debug("Doctor JIM created");
  }

  /**
   * 

* isStrict *

* * @return a boolean. */ public boolean isStrict( ) { return mStrict; } public boolean removeUnusedImports( ) { return removeUnusedImports; } public void setRemoveUnusedImports( boolean pRemoveUnusedImports ) { removeUnusedImports = pRemoveUnusedImports; } /** *

* setStrict *

* * @param pStrict a boolean. */ public void setStrict( boolean pStrict ) { mStrict = pStrict; } /** * Get encoding used to read/write files * * @return encoding */ public String getEncoding( ) { return mEncoding; } /** * Sets encoding to use for file reading and writing * * @param pEncoding the encoding to use, null means platform default */ public void setEncoding( String pEncoding ) { mEncoding = pEncoding; } /** * Organized imports from InputFile and writes processed data to OutputFile * * @param pInputFile the file to read, must not be null * @param pOutputFile the file to write to, must not be null * @throws ParseException in case of an parsing errors * @throws IOException in case of an I/O error * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM */ public void organizeImports( File pInputFile, File pOutputFile, List pImportGroups ) throws ParseException, IOException { final String lInput = FileUtils.readFileToString(pInputFile, getEncoding()); String lOutput = organizeImports(lInput, pImportGroups); FileUtils.writeStringToFile(pOutputFile, lOutput, getEncoding()); } /** * Organized imports from the given String * * @param pInput a {@link java.lang.String} object. * @return a {@link java.lang.String} object. * @throws de.plushnikov.doctorjim.javaparser.ParseException in case of an parsing errors */ public String organizeImports( String pInput, List pImportGroups ) throws ParseException { sLogger.debug("Started initialization of the parser"); // create Parser and initialize with input string ParserConfiguration lConfiguration = new ParserConfiguration(); lConfiguration.setTabSize(1); JavaParser lParser = new JavaParser(lConfiguration); ParseResult lParseResult = lParser.parse(new StringReader(pInput)); if (lParseResult.isSuccessful()) { CompilationUnit lCompilationUnit = lParseResult.getResult().get(); // parse the string and collect all informations sLogger.debug("Parser successfully finished"); // Main package Optional lPackageDeclaration = lCompilationUnit.getPackageDeclaration(); final String lMainPackageName; final PackageDeclaration lPackage; if (lPackageDeclaration.isPresent()) { lPackage = lPackageDeclaration.get(); lMainPackageName = lPackageDeclaration.get().getNameAsString(); } else { lPackage = null; lMainPackageName = DEFAULT_PACKAGE; } if (StringUtils.isNotBlank(lMainPackageName)) { sLogger.debug("Found package declaration: " + lMainPackageName); } else { sLogger.debug("Found no package declaration"); } // All Imports, which are already defined NodeList lDeclaredImports = lCompilationUnit.getImports(); // final Collection lImports = lParser.getImports(); Collection lOriginalImports = new HashSet(lDeclaredImports.size()); // Collect imports Collection lStarImports = new HashSet(lDeclaredImports.size()); for (ImportDeclaration lImport : lDeclaredImports) { String lImportValue; if (lImport.isStatic()) { lImportValue = "static " + lImport.getNameAsString(); } else { lImportValue = lImport.getNameAsString(); } if (lImport.isAsterisk()) { lImportValue = lImportValue + ".*"; lStarImports.add(lImportValue); } lOriginalImports.add(lImportValue); } // add some of basic java.lang types initializeJavaLang(lOriginalImports); // if strict and there are star imports, break and return immediately if (isStrict() && !lStarImports.isEmpty()) { sLogger.debug("Doctor JIM is in strict modus and file contains star imports -> exiting"); return pInput; } // extract head section of the file (everything before first import or just before package end) String lHeadSection = extractHeadSection(pInput, lPackage, lDeclaredImports); // extract import section of the file (everything between begin of first import and end of last import) String lImportsSection = extractImportsSection(pInput, lDeclaredImports); // check for safe import section (it means where are no comments between imports) boolean lImportsAreSafe = verifyInputSection(lImportsSection); // prepare place for all java parser imports Collection lGeneratedImports = new HashSet(); // add all local defines Types, because they are already 'imported' @SuppressWarnings("rawtypes") Collection lLocalTypes = lCompilationUnit.findAll(TypeDeclaration.class); List lUsedTypes = new ArrayList<>(); for (TypeDeclaration localType : lLocalTypes) { if (!StringUtils.isBlank(lMainPackageName)) { lGeneratedImports.add(lMainPackageName + '.' + localType.getNameAsString()); } else { lGeneratedImports.add(localType.getNameAsString()); } } for (ClassOrInterfaceType lNext : lCompilationUnit.findAll(ClassOrInterfaceType.class)) { String lTypeName = lNext.getNameWithScope(); if (lUsedTypes.contains(lTypeName) == false) { lUsedTypes.add(lTypeName); } } for (FieldAccessExpr lNext : lCompilationUnit.findAll(FieldAccessExpr.class)) { String lTypeName = lNext.toString(); List lChildNodes = lNext.getChildNodes(); // "this" expressions are not relevant to us. if (lChildNodes.size() > 0 && (lChildNodes.get(0) instanceof ThisExpr == false && lChildNodes.get(0) instanceof BinaryExpr == false && lNext.getParentNode().get() instanceof MemberValuePair == false)) { lUsedTypes.add(lTypeName); } } for (AnnotationExpr lNext : lCompilationUnit.findAll(AnnotationExpr.class)) { String lTypeName = lNext.getChildNodes().get(0).toString(); if (lUsedTypes.contains(lTypeName) == false) { lUsedTypes.add(lTypeName); } List lFieldAccessExpressions = lNext.findAll(FieldAccessExpr.class); for(FieldAccessExpr lNextExpr : lFieldAccessExpressions) { String lString = lNextExpr.toString(); if(lUsedTypes.contains(lString)== false) { lUsedTypes.add(lString); } } } for (MethodCallExpr lNext : lCompilationUnit.findAll(MethodCallExpr.class)) { String lTypeName = lNext.getChildNodes().get(0).toString(); if (lUsedTypes.contains(lTypeName) == false && lTypeName.endsWith(")") == false) { lUsedTypes.add(lTypeName); } } // Prefer entries from java.lang.* over all others. List lOrderedTypes = lUsedTypes.stream().filter(s -> s.startsWith("java.lang.")) .collect(Collectors.toList()); lUsedTypes.removeAll(lOrderedTypes); lOrderedTypes.addAll(lUsedTypes); List lTypesToProcess = new ArrayList<>(); for (String lNext : lOrderedTypes) { String[] lParts = lNext.split("\\."); if (lParts.length > 1) { boolean lFound = false; for (String lPart : lParts) { if (lPart.matches("\\p{Lu}.*")) { lFound = true; break; } } if (lFound) { Optional lImport = lOriginalImports.stream().filter(e -> e.endsWith("." + lParts[0])).findFirst(); String lImportValue; if (lImport.isPresent()) { lImportValue = lImport.get(); } else { lImportValue = lNext; } if (lTypesToProcess.contains(lImportValue) == false) { lTypesToProcess.add(lImportValue); } } } // Types which already have an explicit import will get lost, if we do not check that. Sadly this will only // work in strict mode. else if (mStrict) { Optional lImport = lOriginalImports.stream().filter(e -> e.endsWith("." + lNext)).findFirst(); if (lImport.isPresent()) { lTypesToProcess.add(lImport.get()); } } } // Filter out constants and inner classes List lFilteredTypes = new ArrayList<>(); for ( String lNext : lTypesToProcess) { String[] lParts = lNext.split("\\."); int lIndex = 0; if (lParts.length > 1) { for (int i = lParts.length - 1; i >= 0; i--) { String lPart = lParts[i]; if (lPart.matches("\\p{Lu}.*")) { lIndex = i; } else { break; } } StringBuffer lBuffer = new StringBuffer(); for (int i = 0; i <= lIndex; i++) { lBuffer.append(lParts[i]); if (i < lIndex) { lBuffer.append("."); } } String lType = lBuffer.toString(); if (lFilteredTypes.contains(lType) == false) { lFilteredTypes.add(lType); } } } Collections.sort(lTypesToProcess); // Filter unused imports. List lUnusedImports = new ArrayList<>(); for (ImportDeclaration lNextDeclaredImport : lDeclaredImports) { String lImport = lNextDeclaredImport.getNameAsString(); if (lNextDeclaredImport.isAsterisk() == false) { // Current import is an explicit import, so it is expected to be part of list of used types if (lTypesToProcess.contains(lImport) == false) { lUnusedImports.add(lImport); } } } // Apply changes to body. String lBody = extractBodySection(pInput, lPackage, lDeclaredImports); for ( String lTypeName : lTypesToProcess) { final String[] lParts = lTypeName.split("\\."); int lCurrentScanToken = lParts.length - 1; // search for last (first from the end) type-part, which starts with upper case letter while (lCurrentScanToken >= 0 && !lParts[lCurrentScanToken].matches("\\p{Lu}.*")) { lCurrentScanToken--; } // be sure, we have find something if (lCurrentScanToken >= 0) { // drop everything after last type-part from the previous calculation final String lLastTypePart = lParts[lCurrentScanToken]; String lImport2Replace = lTypeName.substring(0, lTypeName.lastIndexOf(lLastTypePart) + lLastTypePart.length()); // apply workaround for Map.Entry import lImport2Replace = changeReplacementWorkaround(lImport2Replace); // import should start with lower case and contains '.' if (lImport2Replace.matches("[\\p{L}&&[^\\p{Lu}]].*\\..*") && !isConflict(lImport2Replace, lOriginalImports, lGeneratedImports)) { final String lImport2ReplaceWith = lImport2Replace.substring(lImport2Replace.lastIndexOf('.') + 1); final String lReplaceSource = "([^\\w\\p{L}\\.\"])" + lImport2Replace.replaceAll("\\.", "\\\\s*\\.\\\\s*") + "([^\\p{L}\"])"; final String lReplaceTarget = "$1" + lImport2ReplaceWith + "$2"; final String lBodyNew = lBody.replaceAll(lReplaceSource, lReplaceTarget); final String lHeadSectionNew = lHeadSection.replaceAll(lReplaceSource, lReplaceTarget); // add new import only if something has really changed in the output if (!lBodyNew.equals(lBody) || !lHeadSectionNew.equals(lHeadSection)) { lGeneratedImports.add(lImport2Replace); lBody = lBodyNew; lHeadSection = lHeadSectionNew; } } } } // prepare result buffer final StringBuilder lBuffer = new StringBuilder(pInput.length()); // add original head if (lHeadSection.length() > 0) { lBuffer.append(lHeadSection); lBuffer.append(NEW_LINE); lBuffer.append(NEW_LINE); } final Set lAllImports = new TreeSet(); lAllImports.addAll(lGeneratedImports); if (!lImportsAreSafe) { if (lImportsSection.length() > 0) { lBuffer.append(lImportsSection); lBuffer.append(NEW_LINE); lBuffer.append(NEW_LINE); } // remove original imports lGeneratedImports.removeAll(lOriginalImports); } else { // add orignal imports lAllImports.addAll(lOriginalImports); } // Remove unused imports if (removeUnusedImports && mStrict) { lAllImports.removeAll(lUnusedImports); } // append new imports String lGeneratedImportsSection = generateImportSection(lAllImports, lMainPackageName, lStarImports, pImportGroups); if (lGeneratedImportsSection.length() > 0) { lBuffer.append(lGeneratedImportsSection); lBuffer.append(NEW_LINE); } // append body of class lBuffer.append(lBody); return lBuffer.toString(); } else { throw new ParseException(lParseResult.getProblems().toString()); } } /** *

* extractHeadSection *

* * @param pInput a {@link java.lang.String} object. * @param pPackage a {@link de.plushnikov.doctorjim.ElementPosition} object. * @param pImports a {@link java.util.Collection} object. * @return a {@link java.lang.String} object. */ protected String extractHeadSection( String pInput, PackageDeclaration pPackage, NodeList pImports ) { ImportDeclaration lFirstImport = null; if (pImports != null && !pImports.isEmpty()) { lFirstImport = pImports.getFirst().get(); } int lInputPosition = 0; if (null != pPackage || null != lFirstImport) { final int lColumn = null == lFirstImport ? pPackage.getEnd().get().column : lFirstImport.getBegin().get().column - 1; final int lLine = null == lFirstImport ? pPackage.getEnd().get().line : lFirstImport.getBegin().get().line; lInputPosition = locatePosition(pInput, lLine, lColumn + 1); } if (sLogger.isDebugEnabled()) { sLogger.debug("Extract Headsection from positions [0," + lInputPosition + "]"); } return StringUtils.stripToEmpty(pInput.substring(0, lInputPosition)); } /** *

* extractImportsSection *

* * @param pInput a {@link java.lang.String} object. * @param pImports a {@link java.util.Collection} object. * @return a {@link java.lang.String} object. */ protected String extractImportsSection( String pInput, NodeList pImports ) { String result = ""; if (pImports != null && !pImports.isEmpty()) { final ImportDeclaration lFirstImport = pImports.getFirst().get(); final ImportDeclaration lLastImport = pImports.getLast().get(); int lStart = locatePosition(pInput, lFirstImport.getBegin().get().line, lFirstImport.getBegin().get().column); int lEnd = locatePosition(pInput, lLastImport.getEnd().get().line, lLastImport.getEnd().get().column); if (sLogger.isDebugEnabled()) { sLogger.debug("Extract Importssection from positions [" + lStart + "," + (lEnd + 1) + "]"); } result = StringUtils.stripToEmpty(pInput.substring(lStart, lEnd + 1)); } else { sLogger.debug("No Importsection to extract"); } return result; } /** *

* extractBodySection *

* * @param pInput a {@link java.lang.String} object. * @param pPackage a {@link de.plushnikov.doctorjim.ElementPosition} object. * @param pImports a {@link java.util.Collection} object. * @return a {@link java.lang.String} object. */ protected String extractBodySection( String pInput, PackageDeclaration pPackage, NodeList pImports ) { // determine last element, after which class body declaration starts NodeWithRange lClassBodyStartsAfterObject = pPackage; if (pImports != null && !pImports.isEmpty()) { lClassBodyStartsAfterObject = pImports.getLast().get(); } // calculate start position of class body int lClassBodyStartPosition = 0; if (null != lClassBodyStartsAfterObject) { lClassBodyStartPosition = locatePosition(pInput, lClassBodyStartsAfterObject.getEnd().get().line, lClassBodyStartsAfterObject.getEnd().get().column + 1); } if (sLogger.isDebugEnabled()) { sLogger.debug("Extract Bodyssection from positions [" + lClassBodyStartPosition + "," + pInput.length() + "]"); } return StringUtils.stripToEmpty(pInput.substring(lClassBodyStartPosition)); } /** *

* verifyInputSection *

* * @param pImportSection a {@link java.lang.String} object. * @return a boolean. */ protected boolean verifyInputSection( String pImportSection ) { return !pImportSection.contains("//") && !pImportSection.contains("/*") && !pImportSection.contains("*/"); } /** *

* isConflict *

* * @param type a {@link java.lang.String} object. * @param importList a {@link java.util.Collection} object. * @param replacedSet a {@link java.util.Collection} object. * @return a boolean. */ protected boolean isConflict( String type, Collection importList, Collection replacedSet ) { return !JAVA_LANG_PACKAGE.equals(extractPackage(type)) && (isConflict(type, replacedSet) || isConflict(type, importList)); } /** *

* isConflict *

* * @param pType a {@link java.lang.String} object. * @param pTestSet a {@link java.util.Collection} object. * @return a boolean. */ protected boolean isConflict( String pType, Collection pTestSet ) { if (pTestSet.contains(pType)) { return false; } for (String importType : pTestSet) { if (!importType.endsWith(STAR_IMPORT) && !importType.startsWith("static ")) { if (!pType.equals(importType)) { String lClassName = importType; final int lPosition = importType.lastIndexOf('.'); if (lPosition > 0) { lClassName = importType.substring(lPosition); } if (pType.endsWith(lClassName)) { return true; } } } } return false; } /** *

* generateImportSection *

* * @param pAllImports a {@link java.util.Set} object. * @param pMainPackage a {@link java.lang.String} object. * @param pStarImports a {@link java.util.Collection} object. * @return a {@link java.lang.String} object. */ protected String generateImportSection( Set pAllImports, String pMainPackage, Collection pStarImports, List pImportGroups ) { StringBuilder lBuffer = new StringBuilder(256); List> lAllGroupedImports = this.groupImports(pAllImports, pImportGroups, true); lAllGroupedImports.addAll(this.groupImports(pAllImports, pImportGroups, false)); int lLoopCount = 1; for (List lNextGroup : lAllGroupedImports) { lLoopCount++; for (String lImport : lNextGroup) { String lImportPackage = extractPackage(lImport); // make sure the import is not redundant, because : // - it is java.lang import (automatically imported) // - it is part of the current package // - there is * import from the same package already if (!JAVA_LANG_PACKAGE.equals(lImportPackage) && !pMainPackage.equals(lImportPackage) && (lImport.endsWith(STAR_IMPORT) || !pStarImports.contains(lImportPackage + STAR_IMPORT))) { lBuffer.append(IMPORT_STATEMENT).append(lImport).append(';').append(NEW_LINE); } } if (lLoopCount < lAllGroupedImports.size()) { lBuffer.append(NEW_LINE); } } return lBuffer.toString(); } public List> groupImports( Set pAllImports, List pImportGroups, boolean pStatic ) { // Reduce imports to those that should be processed (static or non-static ones) pAllImports = pAllImports.stream().filter(f -> f.startsWith("static ") == pStatic).collect(Collectors.toSet()); List> lGroupedImports = new ArrayList<>(); List lLeftOvers = new ArrayList<>(pAllImports); for (String lNextGroup : pImportGroups) { List lImports = pAllImports.stream().filter(e -> e.startsWith(lNextGroup)).collect(Collectors.toList()); Collections.sort(lImports); lGroupedImports.add(lImports); lLeftOvers.removeAll(lImports); } Collections.sort(lLeftOvers); lGroupedImports.add(lLeftOvers); return lGroupedImports; } /** *

* Calculates position of a line:column tuple *

* * @param pInput original Data-string * @param pLine a line in this string * @param pColumn a column in this string * @return Position of a character in the string */ protected int locatePosition( String pInput, int pLine, int pColumn ) { int result = pColumn; if (pLine > 0) { result += StringUtils.ordinalIndexOf(pInput, "\n", pLine - 1); } return result; } /** *

* Extracts package name from the type declaration *

* * @param pImportType a declaration of some type. * @return package name of this type */ protected String extractPackage( String pImportType ) { final int index = pImportType.lastIndexOf('.'); String typePackage = ""; if (-1 < index) { typePackage = pImportType.substring(0, index); } return typePackage; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy