package org.hybridlabs.source.beautifier;
/**
* Copyright 2008 hybrid labs
*
* 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.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hybridlabs.file.FileHelper;
import org.hybridlabs.source.beautifier.replacement.DefaultTypeReplacementStrategy;
import org.hybridlabs.source.beautifier.replacement.TypeReplacementStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.hunsicker.jalopy.language.antlr.JavaNode;
/**
* Simple oAW beautifier implementation based on Jalopy and the antlr JavaNode. The
* implementation substitutes fully qualified class names (as long as there is
* no conflict) with the short name and the according import statement.
*
* @author Karsten Klein, hybrid labs
* @author Jan Labrie
*/
public class JavaImportBeautifierImpl extends JavaBeautifier implements BeautifierWithConvention {
private static final Logger LOG = LoggerFactory.getLogger(JavaImportBeautifierImpl.class);
private static final String LINESEP = System.getProperty("line.separator");
private static final CharacterSequence DEFAULT_PACKAGE = new CharacterSequence(new StringBuffer());
private static Set<Integer> startSequenceTypeSet = new HashSet<Integer>();
private static Set<Integer> terminateSequenceTypeSet = new HashSet<Integer>();
static {
startSequenceTypeSet.add(new Integer(PACKAGE));
startSequenceTypeSet.add(new Integer(IMPORT_STATEMENT));
startSequenceTypeSet.add(new Integer(GENERIC_UPPER_BOUNDS));
startSequenceTypeSet.add(new Integer(EXTENDS_CLAUSE));
startSequenceTypeSet.add(new Integer(IMPLEMENTS_CLAUSE));
startSequenceTypeSet.add(new Integer(TYPE));
startSequenceTypeSet.add(new Integer(EXPRESSION));
startSequenceTypeSet.add(new Integer(NEW));
startSequenceTypeSet.add(new Integer(PACKAGE_ANNOTATION));
startSequenceTypeSet.add(new Integer(ANNOTATION));
startSequenceTypeSet.add(new Integer(ANNOTATION_VALUE));
startSequenceTypeSet.add(new Integer(CAST));
startSequenceTypeSet.add(new Integer(CLASS));
startSequenceTypeSet.add(new Integer(AT));
startSequenceTypeSet.add(new Integer(SEMI));
startSequenceTypeSet.add(new Integer(STATIC_IMPORT_STATEMENT));
startSequenceTypeSet.add(new Integer(DOT));
startSequenceTypeSet.add(new Integer(COMMA));
startSequenceTypeSet.add(new Integer(THROWS));
}
static {
// these terminate a sequence AND inherit the scope of the current (separator types)
terminateSequenceTypeSet.add(new Integer(10));
terminateSequenceTypeSet.add(new Integer(11));
terminateSequenceTypeSet.add(new Integer(39));
terminateSequenceTypeSet.add(new Integer(45));
terminateSequenceTypeSet.add(new Integer(DOT));
terminateSequenceTypeSet.add(new Integer(100));
terminateSequenceTypeSet.add(new Integer(109));
terminateSequenceTypeSet.add(new Integer(COMMA));
}
private boolean isOrganizeImports = true;
private boolean isStrict = true;
private boolean isFormat = true;
private Map<Integer, TypeReplacementStrategy> strategyMap = new HashMap<Integer, TypeReplacementStrategy>();
public JavaImportBeautifierImpl() {
// populate strategy map
strategyMap.put(new Integer(NEW), new DefaultTypeReplacementStrategy("[\\,\\<\\)\\[\\<\\(\\s]"));
strategyMap.put(new Integer(GENERIC_UPPER_BOUNDS), new DefaultTypeReplacementStrategy( "[\\(\\,\\<\\[\\,\\>\\s]"));
strategyMap.put(new Integer(EXTENDS_CLAUSE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
strategyMap.put(new Integer(IMPLEMENTS_CLAUSE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
strategyMap.put(new Integer(THROWS), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\<\\)\\.\\s*]"));
strategyMap.put(new Integer(TYPE), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\)\\[\\<\\;\\s\\.]"));
strategyMap.put(new Integer(EXPRESSION), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\<\\)\\.\\s*]"));
strategyMap.put(new Integer(ANNOTATION), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
strategyMap.put(new Integer(AT), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
strategyMap.put(new Integer(CAST), new DefaultTypeReplacementStrategy("[\\(\\)\\>\\<\\[\\,\\;\\{\\s\\.]"));
strategyMap.put(new Integer(ANNOTATION_VALUE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s\\.]"));
}
public boolean isFormat() {
return isFormat;
}
public void setFormat(boolean isFormat) {
this.isFormat = isFormat;
}
public boolean isOrganizeImports() {
return isOrganizeImports;
}
public void setOrganizeImports(boolean isOrganizeImports) {
this.isOrganizeImports = isOrganizeImports;
}
public boolean isStrict() {
return isStrict;
}
public void setStrict(boolean isOrganizeOnWildcards) {
this.isStrict = isOrganizeOnWildcards;
}
public void beautify(CharacterSequence sb) {
File file = null;
try {
file = File.createTempFile("hybridlabs-beautifier", ".java");
if (isOrganizeImports()) {
organizeImports(sb, file);
}
if (isFormat()) {
format(sb, file);
}
} catch (Exception e) {
handleError(e, sb);
} catch (Error e) {
handleError(e, sb);
} finally {
if (file != null) {
file.delete();
}
}
}
/**
* Beautifies the source file and writes the result to the target file.
* Source and target may be identical.
*
* @param sourceFile
* @param targetFile
* @throws FileNotFoundException
* @throws IOException
*/
public void beautify(File sourceFile, File targetFile)
throws FileNotFoundException, IOException {
StringBuffer buffer = FileHelper.loadStringBuffer(sourceFile);
CharacterSequence sequence = new CharacterSequence(buffer);
beautify(sequence);
String targetPath = sourceFile.getAbsolutePath();
if (targetFile != null) {
targetPath = targetFile.getAbsolutePath();
}
FileWriter writer = new FileWriter(new File(targetPath));
writer.write(sequence.toString());
writer.close();
}
private void handleError (Throwable e, CharacterSequence sb) {
LOG.error(e.getClass().getSimpleName()+" occured during beautification. Content:" + LINESEP + sb.subSequence(0, Math.min(160, sb.length())) + "...");
if (LOG.isDebugEnabled()) {
LOG.debug("Error during beautification.", e);
} else if (e.getMessage()!=null) {
LOG.error ("Error message: "+e.getMessage());
}
}
private void organizeImports(CharacterSequence sequence, File file) {
// create formatter context for this file
BeautifierContext formatterContext = new BeautifierContext();
// use formatter context to traverse the abstract syntax tree
JavaNode node = createJavaNode(sequence, file);
traverseAst(node, formatterContext, 0);
Set<TypeContext> sequences = formatterContext.getSequences();
// dump all detected sequences to system out in DEBUG mode
if (LOG.isDebugEnabled()) {
printSequences(sequences);
}
// extract package from ast
CharacterSequence currentPackage = DEFAULT_PACKAGE;
for (TypeContext typeContext : sequences) {
if (typeContext.getType() == PACKAGE_ANNOTATION) {
currentPackage = typeContext.getQualifiedName();
break;
}
}
// determine absolute position of package declaration
int packageEndPos = findPositionInCharacterSequence(sequence,
formatterContext.getPackageLine(), formatterContext.getPackageEnd());
// determine absolute position of import declaration
int importEndPos = findPositionInCharacterSequence(sequence,
formatterContext.getImportEndLine(), formatterContext.getImportEndColumn());
// split the file in header (including the package declaration) ...
CharacterSequence result;
if (DEFAULT_PACKAGE.equals(currentPackage)) {
result = new CharacterSequence(new StringBuffer());
packageEndPos = 0;
} else {
result = sequence.subCharacterSequence(0, packageEndPos);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Package: " + currentPackage + "[" + packageEndPos + "]");
LOG.debug("Imports end at: " + importEndPos);
}
if (importEndPos == -1) {
importEndPos = packageEndPos;
}
CharSequence imports = sequence.subSequence(packageEndPos, importEndPos);
// do not organize anything if there is a wildcard import - this could lead to ambiguity
if (isStrict && imports.toString().contains(".*")) {
LOG.info("Did not organize import "+file.getAbsolutePath()+" - file contains wildcard imports.");
} else {
// and the body part (imports, comments, classes/interfaces)
CharSequence body = sequence.subSequence(importEndPos, sequence.length());
// extract the detected imports sequences
Set<CharacterSequence> importTypes = extractImports(sequences);
// set for monitoring the already replaced types
Set<CharacterSequence> replacedSet = new TreeSet<CharacterSequence>();
for (TypeContext typeContext : sequences) {
if (typeContext.getType() == CLASS) {
if (currentPackage == null) {
importTypes.add(typeContext.getQualifiedName());
} else {
CharacterSequence sb = new CharacterSequence(new StringBuffer(100));
sb.append(currentPackage);
sb.append('.');
sb.append(typeContext.getQualifiedName());
importTypes.add(sb);
}
}
}
body = replaceFullQualifiedClassNames(body, sequences, importTypes, replacedSet);
if (imports.length() > 0) {
result.append(imports);
}
// populate header with imports
int numNewImports = 0;
for (CharacterSequence type : replacedSet) {
if (!isRedundant(type, importTypes)) {
int index = type.lastIndexOf('.');
CharSequence typePackage = index == -1 ? DEFAULT_PACKAGE : type.subSequence(0, index);
// make sure the import is not redundant, because it is part of the current package
// and also the java.lang package does not need to be imported
if (!currentPackage.equals(typePackage) && !typePackage.equals("java.lang")) {
result.append("import ");
result.append(type);
result.append(";");
result.append(LINESEP);
numNewImports += 1;
}
}
}
// separate existing imports from added imports
if (numNewImports > 0) {
result.append(LINESEP);
}
// supplement the rest of the source
result.append(body);
sequence.set(result);
}
}
private CharSequence replaceFullQualifiedClassNames(CharSequence body, Set<TypeContext> sequences, Set<CharacterSequence> importTypes, Set<CharacterSequence> replacedSet) {
// body consists of import, comment, classes etc
// the existing imports should not be touched by the replacement
Set<CharacterSequence> conflictCache = new HashSet<CharacterSequence>();
// At first replace all java.lang imports
for (TypeContext typeContext : sequences) {
if (typeContext.getQualifiedName().startsWith("java.lang")) {
body = replaceFullQualifiedClassNames(typeContext, body, importTypes, replacedSet, conflictCache);
}
}
// And then the 'ordinary' ones
for (TypeContext typeContext : sequences) {
body = replaceFullQualifiedClassNames(typeContext, body, importTypes, replacedSet, conflictCache);
}
return body;
}
private CharSequence replaceFullQualifiedClassNames(
TypeContext typeContext,
CharSequence body,
Set<CharacterSequence> importTypes,
Set<CharacterSequence> replacedSet,
Set<CharacterSequence> conflictCache) {
CharacterSequence type = typeContext.getQualifiedName();
if (!replacedSet.contains(type)) {
if (typeContext.isValidType()) {
// ensure there are no conflicts
if (!isConflict(type, importTypes, replacedSet, conflictCache)) {
// access the replacement strategy
TypeReplacementStrategy strategy = (TypeReplacementStrategy)
strategyMap.get(new Integer(typeContext.getType()));
if (strategy != null) {
String replacement = strategy.composeReplace(type);
Pattern pattern = Pattern.compile(strategy
.composeMatch(type));
Matcher matcher = pattern.matcher(body);
// check whether the pattern was matched
if (matcher.find()) {
// match and replace the type by the short form (in the sub string)
body = matcher.replaceAll(replacement);
// mark type as replaced
replacedSet.add(type);
}
}
}
}
}
return body;
}
private Set<CharacterSequence> extractImports(Set<TypeContext> sequences) {
if (LOG.isDebugEnabled()) {
LOG.debug("These are the existing imports:");
}
Set<CharacterSequence> importTypes = new TreeSet<CharacterSequence>();
for (TypeContext typeContext : sequences) {
if (typeContext.getType() == IMPORT_STATEMENT) {
CharacterSequence sb = typeContext.getQualifiedName();
importTypes.add(sb);
LOG.debug("{}", sb.toString());
} else if (typeContext.getType() == STATIC_IMPORT_STATEMENT) {
CharacterSequence sb = new CharacterSequence(new StringBuffer(200));
sb.append("static ");
sb.append(typeContext.getQualifiedName());
importTypes.add(sb);
LOG.debug("{}", sb);
}
}
return importTypes;
}
private void printSequences(Set<TypeContext> sequences) {
LOG.debug("These are the symbols that are candidate to be replaced by imports:");
for (TypeContext typeContext : sequences) {
LOG.debug("{}", typeContext);
}
}
private boolean isConflict(CharacterSequence type, Set<CharacterSequence> importList, Set<CharacterSequence> replacedSet, Set<CharacterSequence> conflictCache) {
if (isConflict(type, replacedSet, conflictCache)) {
return true;
}
if (isConflict(type, importList, conflictCache)) {
return true;
}
return false;
}
private boolean isConflict(CharacterSequence type, Set<CharacterSequence> testSet, Set<CharacterSequence> conflictCache) {
if (testSet.contains(type)) {
return false;
}
if (conflictCache.contains(type)) {
return true;
}
for (CharacterSequence importType : testSet) {
if (!importType.endsWith('*')) {
if (!type.equals(importType)) {
CharSequence t = importType
.substring(importType.lastIndexOf('.') + 1);
if (type.endsWith("." + t) && !importType.startsWith("static ")) {
conflictCache.add(type);
return true;
}
}
}
}
return false;
}
/**
* Method to calculate whether a symbol is already covered by a set of import sequences
* @param type The type which is checked
* @param importList The list of import statements in which regard the checking is done
* @return true when the symbol is covered by the import list
*/
private final boolean isRedundant(CharacterSequence type, Set<CharacterSequence> importList) {
for (CharacterSequence importType : importList) {
if (importType.equals(type) ||
importType.endsWith('*') &&
notLastPart(type).equals(notLastPart(importType))) {
return true;
}
}
return false;
}
private String notLastPart(CharacterSequence charSeq) {
String result= "";
int i = charSeq.lastIndexOf('.');
if (i >= 0) {
result = charSeq.subCharacterSequence(0, i).toString();
}
return result;
}
private int traverseAst(JavaNode ast, BeautifierContext context, int depth) {
// print this node
if (LOG.isDebugEnabled()) {
preparePrintln(depth);
LOG.debug("{}", ast);
}
if (context.isProcessed(ast)) {
return -1;
}
context.preProcess(ast);
// check whether this subtree is to be ignored completely
Integer astType = new Integer(ast.getType());
if (terminateSequenceTypeSet.contains(astType)) {
context.terminateCurrentSequence(ast, context.getCurrentTypeContext().getType());
} else if (startSequenceTypeSet.contains(astType)) {
context.terminateCurrentSequence(ast, -1);
}
if (LOG.isDebugEnabled()) {
LOG.debug("TRAVERSING: " + ast.getType());
}
// traverse all children
JavaNode child = (JavaNode) ast.getFirstChild();
if (child != null) {
traverseAst(child, context, depth + 1);
}
context.postProcess(ast);
// print if a type was detected
if (ast.getType() == IDENT) {
context.addToCurrentTypeContext(ast);
if (LOG.isDebugEnabled()) {
LOG.debug(preparePrintln(depth)+context.getCurrentTypeContext());
}
}
// continue visiting the tree (traverse siblings)
JavaNode next = (JavaNode) ast.getNextSibling();
while (next != null) {
traverseAst(next, context, depth);
next = (JavaNode) next.getNextSibling();
}
return -1;
}
private String preparePrintln(int depth) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < depth; i++) {
result.append(" ");
}
return result.toString();
}
}