/** * <copyright> * * Copyright (c) 2009, 2010 Springsite BV (The Netherlands) and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Martin Taal - Initial API and implementation * * </copyright> * * $Id: ImportResolver.java,v 1.13 2011/08/25 12:34:30 mtaal Exp $ */ package org.eclipse.emf.texo.generator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; import org.eclipse.emf.texo.utils.Check; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.ImportDeclaration; import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; import org.eclipse.jdt.ui.PreferenceConstants; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; /** * Is responsible for creating import statements in a source file based on the fully qualified names present in the * source file. The logic: * <ul> * <li>tries to retain and re-use existing import statements</li> * <li>will not create an import statement if not necessary, so types in the same package are not translated to an * import statement.</li> * <li>will prevent import collisions by</li> * </ul> * * This resolver does not check if the referred-to types actually exist. * * The resolver goes through the following steps: * <ol> * <li>Collect current import statements and all qualified names</li> * <li>Compare the qualified names with current import statements, determine which ones are re-used and which one to * remove</li> * <li>Create new import declarations for qualified names for which no import statement could be found</li> * <li>Sort the importdeclaration according to the project settings</li> * <li>Adapt the source file</li> * </ol> * * @author <a href="mtaal@elver.org">Martin Taal</a> */ public class ImportResolver { private static final Pattern SEMICOLON_PATTERN = Pattern.compile(";"); //$NON-NLS-1$ private static final Pattern DOT_PATTERN = Pattern.compile("\\."); //$NON-NLS-1$ private static final String JAVA_LANG = "java.lang"; //$NON-NLS-1$ private static final String DOT = "."; //$NON-NLS-1$ private String source; private CompilationUnit compilationUnit; private IJavaProject javaProject; /** * Determine all needed imports, retain existing imports if needed, update import section in the source and replace * all qualified names with their non-qualified equivalent (if there is an import statement). * * @return the updated source */ public String resolve() { Check.isNotNullArgument(source, "source"); //$NON-NLS-1$ Check.isNotNullArgument(javaProject, "javaProject"); //$NON-NLS-1$ // get the package name and the list of existing and needed types final ImportReferenceCollector importReferenceCollector = new ImportReferenceCollector(); compilationUnit.accept(importReferenceCollector); // get the list of needed imports because of already present // non-qualified names. final List<ImportDeclaration> importDeclarations = determineCurrentNeededImports( importReferenceCollector.getExistingImports(), importReferenceCollector.getQualifiedTypes()); final List<String> cleanedQualifedNames = cleanQualifiedNames(importReferenceCollector.getQualifiedTypes()); // now determine which new ones should be added and if so prevent // collisions final List<String> importedNonQualifiedNames = importedNonQualifiedNames(importDeclarations); final List<String> namesToImport = new ArrayList<String>(); for (String declaredTypeName : importReferenceCollector.getUnqualifiedDeclaredTypes()) { importedNonQualifiedNames.add(getLastSegment(declaredTypeName)); } // add all the retained imports also to the list for (final ImportDeclaration importDeclaration : importDeclarations) { final String nameToImport = importDeclaration.getName().getFullyQualifiedName(); if (!namesToImport.contains(nameToImport)) { namesToImport.add(nameToImport); } } // handle a special case that there is a collision between a class in the same // package and a class imported from another package. In this case the class imported // from the other package should not be unqualified. This is accomplished by setting the // imports for the same package first, then later imports are recognized as collisions final String packageName = importReferenceCollector.getPackageName(); Collections.sort(cleanedQualifedNames, new QualifiedNameSorter(packageName)); for (final String qualifiedName : cleanedQualifedNames) { // already done final String correctedQualifiedName = getCorrectQualifiedName(qualifiedName); if (namesToImport.contains(correctedQualifiedName)) { continue; } // assume that it is covered by an already existing import if (!correctedQualifiedName.contains(DOT)) { continue; } final String lastSegment = getLastSegment(correctedQualifiedName); if (importedNonQualifiedNames.contains(lastSegment)) { // do not import these names to prevent collisions continue; } // if only one dot then also do not import // this is for example values.length (where values is an array) if (correctedQualifiedName.indexOf(DOT) == correctedQualifiedName.lastIndexOf(DOT)) { continue; } // prevent collisions later importedNonQualifiedNames.add(lastSegment); namesToImport.add(correctedQualifiedName); } final AST ast = compilationUnit.getAST(); final List<ImportDeclaration> newImports = new ArrayList<ImportDeclaration>(); for (final String nameToImport : namesToImport) { // don't do these ones if (inPackage(packageName, nameToImport)) { continue; } final Name name = ast.newName(nameToImport); final ImportDeclaration importDeclaration = ast.newImportDeclaration(); importDeclaration.setOnDemand(false); importDeclaration.setStatic(false); importDeclaration.setName(name); newImports.add(importDeclaration); } try { String newSource = updateSource(namesToImport, newImports, ast, importReferenceCollector.getPackageName()); // DIRTY HACK: solve an issue that if an EPackage has a sub package with the same // name that the import resolving of the subpackage goes wrong // for example the KdmModelPackage has a subpackage with the same name: KdmModelPackage // this results in this line in the source code: // kdm.KdmModelPackage.initialize(); // which is incorrect for (String qualifiedName : cleanedQualifedNames) { // find the last two segments if (!qualifiedName.contains(DOT)) { continue; } final String[] parts = DOT_PATTERN.split(qualifiedName); if (parts.length > 1) { final String part1 = parts[parts.length - 2]; final String part2 = parts[parts.length - 1]; { final String searchString = "\t" + part1 + DOT + part2 + ".initialize();"; //$NON-NLS-1$ //$NON-NLS-2$ if (newSource.contains(searchString)) { newSource = newSource.replace(searchString, "\t" + qualifiedName + ".initialize();"); //$NON-NLS-1$ //$NON-NLS-2$ } } { final String searchString = " " + part1 + DOT + part2 + ".initialize();"; //$NON-NLS-1$ //$NON-NLS-2$ if (newSource.contains(searchString)) { newSource = newSource.replace(searchString, " " + qualifiedName + ".initialize();"); //$NON-NLS-1$ //$NON-NLS-2$ } } { final String searchString = "\n" + part1 + DOT + part2 + ".initialize();"; //$NON-NLS-1$ //$NON-NLS-2$ if (newSource.contains(searchString)) { newSource = newSource.replace(searchString, "\n" + qualifiedName + ".initialize();"); //$NON-NLS-1$ //$NON-NLS-2$ } } } } return newSource; } catch (final Exception e) { throw new IllegalStateException(e); } } /** * @param packageName * package name of the current source * @param qualifiedName * the qualified name to check against the package name #return true if the qualifiedName is a type directly * contained in the package denoted by the packageName. */ protected boolean inPackage(final String packageName, final String qualifiedName) { final int index = qualifiedName.lastIndexOf(DOT); if (index == -1) { return false; } final String qualifyingPart = qualifiedName.substring(0, index); return qualifyingPart.equals(packageName); } /** * Update the source ({@link #getSource()}): * <ul> * <li>replace the import declarations with new ones (in the correct sort order)</li> * <li>replace all qualified and imported names in the source with their simple variant.</li> * </ul> * * @param allImportedNames * the list of the qualified names which are imported * @param newImports * the new set of import declarations (also contain import declarations from the original source which can be * retained). * @param ast * the abstract syntax tree of the original source * @param packageName * the java package of the source * @return the updated source */ protected String updateSource(final List<String> allImportedNames, final List<ImportDeclaration> newImports, final AST ast, final String packageName) throws BadLocationException { final ASTRewrite astRewriter = ASTRewrite.create(ast); final ImportChangeCollector changeCollector = new ImportChangeCollector(); changeCollector.setAst(ast); changeCollector.setAstRewriter(astRewriter); changeCollector.setImportedNames(allImportedNames); changeCollector.setPackageName(packageName); compilationUnit.accept(changeCollector); final IDocument doc = new Document(source); // now do the importstatements themselve // first remove all the current ones final ListRewrite listRewrite = astRewriter.getListRewrite(compilationUnit, CompilationUnit.IMPORTS_PROPERTY); for (final Object obj : compilationUnit.imports()) { listRewrite.remove((ImportDeclaration) obj, null); } // then add them all using the sorted list final List<ImportDeclaration> sortedImports = sortImports(newImports); for (final ImportDeclaration importDeclaration : sortedImports) { listRewrite.insertLast(importDeclaration, null); } // computation of the text edits final TextEdit edits = astRewriter.rewriteAST(doc, getJavaProject().getOptions(true)); // computation of the new source code edits.apply(doc); return doc.get(); } /** * Sorts a list of import declarations taking into account the import order defined for the project ( * {@link #getJavaProject()}), in addition sort alphabetically. * * @param imports * the import declarations to sort * @return the sorted import declarations */ protected List<ImportDeclaration> sortImports(final List<ImportDeclaration> imports) { // first have them by alphabet final List<ImportDeclaration> alphabeticSortedImports = new ArrayList<ImportDeclaration>(imports); Collections.sort(alphabeticSortedImports, new ImportDeclarationComparator()); // now use the preferences String order = PreferenceConstants.getPreference(PreferenceConstants.ORGIMPORTS_IMPORTORDER, javaProject); if (order.endsWith(";")) { //$NON-NLS-1$ order = order.substring(0, order.length() - 1); } final String[] orderedParts = SEMICOLON_PATTERN.split(order, -1); final List<ImportDeclaration> sortedImports = new ArrayList<ImportDeclaration>(); for (String orderPart : orderedParts) { if (orderPart.trim().endsWith("*")) { //$NON-NLS-1$ orderPart = orderPart.trim().substring(0, orderPart.trim().length() - 1); } final List<ImportDeclaration> toRemove = new ArrayList<ImportDeclaration>(); for (final ImportDeclaration importDeclaration : alphabeticSortedImports) { if (importDeclaration.getName().getFullyQualifiedName().startsWith(orderPart)) { sortedImports.add(importDeclaration); toRemove.add(importDeclaration); } } alphabeticSortedImports.removeAll(toRemove); } // add the remaining sortedImports.addAll(alphabeticSortedImports); return sortedImports; } // get the already imported names to prevent collisions private List<String> importedNonQualifiedNames(final List<ImportDeclaration> importDeclarations) { final List<String> result = new ArrayList<String>(); for (final ImportDeclaration importDeclaration : importDeclarations) { // ignore these if (importDeclaration.isOnDemand() || importDeclaration.isStatic()) { continue; } result.add(getLastSegment(importDeclaration.getName().getFullyQualifiedName())); } return result; } /** * Strips a static member name from a fully qualified name (or returns the fully qualified name if there is no such * static member). Example: * <ul> * <li>org.eclipse.emf.texo.Library returns org.eclipse.emf.texo.Library</li> * <li>org.eclipse.emf.texo.Library.BOOK_INT_VALUE also returns org.eclipse.emf.texo.Library</li> * </ul> * Note: uses the heuristic that the class name in a fully qualified name is the first segment starting with an * upper-case character. * * @param qualifiedName * the qualified name to correct * @return the fully qualified class name */ protected String getCorrectQualifiedName(final String qualifiedName) { if (!qualifiedName.contains(DOT)) { return qualifiedName; } final String[] parts = DOT_PATTERN.split(qualifiedName); final StringBuilder result = new StringBuilder(); for (final String part : parts) { if (result.length() > 0) { result.append(DOT); } result.append(part); if (Character.isUpperCase(part.toCharArray()[0])) { return result.toString(); } } return result.toString(); } /** * Gets the last segment denoting the class plus any static member. Examples: * <ul> * <li>org.eclipse.emf.texo.Library returns Library</li> * <li>org.eclipse.emf.texo.Library.BOOK_INT_VALUE returns Library.BOOK_INT_VALUE</li> * </ul> * * The last segment is identified by checking if the first character of a segment is an upper-case. * * @param qualifiedName * the fully qualified name of a type/static value used in the source * @return the last segment of a qualified name denoting the type name (plus any constant name) * @see #getCorrectQualifiedName(String) */ protected String getLastSegment(final String qualifiedName) { if (qualifiedName.contains(DOT)) { final String correctedQualifiedName = getCorrectQualifiedName(qualifiedName); // the same no special handling if (correctedQualifiedName.length() == qualifiedName.length()) { final int index = qualifiedName.lastIndexOf(DOT); return qualifiedName.substring(1 + index); } // special case for example: // org.eclipse.emf.texo.Library.BOOK_INT_VALUE // should return Library.BOOK_INT_VALUE // while the import should be org.eclipse.emf.texo.Library // also handle this case: // org.eclipse.emf.texo.Library.Feature.WRITER final int index = correctedQualifiedName.lastIndexOf(DOT); return qualifiedName.substring(1 + index); } return qualifiedName; } /** * Cleans a list of qualified names from names without dot or the ones which start with java.lang. * * @param qualifiedNames * the list of qualified names to check * @return a cleaned list */ protected List<String> cleanQualifiedNames(final List<String> qualifiedNames) { final List<String> cleanList = new ArrayList<String>(); for (final String qualifiedName : qualifiedNames) { if (!isJavaLangClass(qualifiedName) && qualifiedName.contains(DOT)) { cleanList.add(qualifiedName); } } return cleanList; } private boolean isJavaLangClass(String className) { if (!className.startsWith(JAVA_LANG)) { return false; } // it starts with java.lang but it can be a sub package // java.lang.reflect if (className.substring(1 + JAVA_LANG.length()).contains(DOT)) { return false; } return true; } /** * Checks which of the imports is needed because there are non-qualified type names. Note, also static and wild card * imports are retained * * @param currentImports * the currentImports in the source * @param qualifiedNames * the qualified names which have been collected * @return the list of imports needed for the existing qualified names */ protected List<ImportDeclaration> determineCurrentNeededImports(final List<ImportDeclaration> currentImports, final List<String> qualifiedNames) { final List<ImportDeclaration> keepImports = new ArrayList<ImportDeclaration>(); // keep the special ones final List<ImportDeclaration> remainingImports = new ArrayList<ImportDeclaration>(currentImports); for (final ImportDeclaration importDeclaration : currentImports) { // special cases must have been added manually if (importDeclaration.isStatic() || importDeclaration.isOnDemand()) { keepImports.add(importDeclaration); remainingImports.remove(importDeclaration); } } // keep the ones which are already imported // note also handles this name: LibraryPackage.BOOK_INT // with this import: org.eclipse.emf.texo.LibraryPackage. for (final String qualifiedName : qualifiedNames) { final int index = qualifiedName.indexOf(DOT); final String checkName; if (index != -1) { checkName = DOT + qualifiedName.substring(0, index); } else { checkName = DOT + qualifiedName; } ImportDeclaration foundDeclaration = null; for (final ImportDeclaration importDeclaration : remainingImports) { if (importDeclaration.getName().getFullyQualifiedName().endsWith(checkName)) { // found keepImports.add(importDeclaration); foundDeclaration = importDeclaration; break; } } if (foundDeclaration != null) { remainingImports.remove(foundDeclaration); } } return keepImports; } public String getSource() { return source; } public void setSource(final String source) { final ASTParser parser = ASTParser.newParser(GeneratorUtils.getASTLevel()); parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setSource(source.toCharArray()); compilationUnit = (CompilationUnit) parser.createAST(null); final IProblem[] problems = compilationUnit.getProblems(); if (problems != null && problems.length > 0) { final StringBuilder sb = new StringBuilder(); for (final IProblem problem : problems) { sb.append(problem.getSourceLineNumber() + ": " //$NON-NLS-1$ + problem.getMessage() + "\n"); //$NON-NLS-1$ } throw new IllegalStateException(source + "\n" + sb.toString()); //$NON-NLS-1$ } this.source = source; } public CompilationUnit getCompilationUnit() { return compilationUnit; } public IJavaProject getJavaProject() { return javaProject; } public void setJavaProject(final IJavaProject javaProject) { this.javaProject = javaProject; } /** * Collects all changes in the source by replacing qualified imported names with simple names. The changes are * collected in the {@link ASTRewrite}. * * The rewrite of the import section is done in the {@link ImportResolver#updateSource(List, List, AST, String)} * method. * * @author mtaal */ protected class ImportChangeCollector extends ASTVisitor { private List<String> importedNames; private AST ast; private ASTRewrite astRewriter; private String packageName; public ImportChangeCollector() { super(true); } @Override public boolean visit(final QualifiedName node) { final String correctedQualifyingName = getCorrectQualifiedName(node.getFullyQualifiedName()); if (isJavaLangClass(correctedQualifyingName) || inPackage(packageName, correctedQualifyingName) || importedNames.contains(correctedQualifyingName)) { // note the last segment should not be called with the corrected // qualifying name but with the original one! // see the special cases for constant imports above. final Name newName = ast.newName(getLastSegment(node.getFullyQualifiedName())); astRewriter.replace(node, newName, null); return false; } return super.visit(node); } @Override public boolean visit(final ImportDeclaration node) { return false; } public void setImportedNames(final List<String> importedNames) { this.importedNames = importedNames; } public void setAst(final AST ast) { this.ast = ast; } public void setAstRewriter(final ASTRewrite astRewriter) { this.astRewriter = astRewriter; } public void setPackageName(final String packageName) { this.packageName = packageName; } } /** * Alphabetically sorts import declarations on the fully qualified name. * * @author mtaal */ protected class ImportDeclarationComparator implements Comparator<ImportDeclaration> { public int compare(final ImportDeclaration id0, final ImportDeclaration id1) { return id0.getName().getFullyQualifiedName().compareTo(id1.getName().getFullyQualifiedName()); } } /** * Is used to sort a list of qualified names so that the qualified names belonging to the java package of the source * are located in the beginning. * * @author mtaal */ private class QualifiedNameSorter implements Comparator<String> { private final String packageName; private QualifiedNameSorter(String packageName) { this.packageName = packageName; } public int compare(String qName1, String qName2) { final boolean name1InPackage = inPackage(packageName, qName1); final boolean name2InPackage = inPackage(packageName, qName2); if (name1InPackage == name2InPackage) { return 0; } else if (name1InPackage) { return -1; } else { return 1; } } } }