/** * Copyright (c) 2005-2009 IBM Corporation 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: * IBM - Initial API and implementation */ package org.eclipse.emf.codegen.util; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.emf.common.EMFPlugin; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.ImportDeclaration; /** * A manager for import declarations in generated code. * * <p>An instance of <code>ImportManager</code> should be created for each compilation unit being generated. It * maintains the list of imports to be added to the compilation unit and associates registered <b>short names</b> with * their corresponding <b>qualified names</b>. * * <p>Common usage of <code>ImportManager</code> is very simple. This example assumes that a <code>StringBuilder</code> * is being used to accumulate the text for a generated source file: * * <pre> * StringBuilder result = new StringBuilder(); * ImportManager importManager = new ImportManager("com.example.target", "Target"); * ... * importManager.markImportLocation(result); * ... * result.append(importManager.getImportedName("com.example.lib.Example", true)); * ... * result.append(importManager.getImportedName("java.lang.String", true)); * ... * result.append(importManager.getImportedName("java.util.Arrays", true)); * ... * result.append(importManager.getImportedName("org.test.Example", true)); * ... * result.append(importManager.getImportedName("org.test.Target", true)); * ... * result.append(importManager.getImportedName("com.example.target.Helper", true)); * ... * importManager.emitSortedImports();</pre> * * <p>The constructor is passed the package name and short name for the compilation unit being generated. The point * where the imports should be inserted is marked by calling <code>markImportLocation</code>. Then, * <code>getImportedName()</code> is called each time a type name is needed, to determine the correct name to use. * Passing <code>true</code> as the second argument instructs the import manager to automatically import the specified * type, if possible. * * <p>In this case, the following names would be returned: * * <ul> * <li><code>Example</code> * <li><code>String</code> * <li><code>Arrays</code> * <li><code>org.test.Example</code> * <li><code>org.test.Target</code> * <li><code>Helper</code> * </ul> * * Note that <code>org.test.Example</code> cannot be shortened because the short name <code>Example</code> is already * taken. Similarly, <code>Target</code> is already taken by the compilation unit, itself. <code>Helper</code> and * <code>String</code> are shortened, but they don't actually require imports. * * <p>Finally, the needed import declarations are inserted by calling <code>emitSortedImports()</code>. In this case, * only two import declarations are produced: * * <pre> * import com.example.lib.Example; * * import java.util.Arrays;</pre> * * <p>In addition to auto-importing, <code>ImportManager</code> supports explicit pre-registration of individual and * wildcard imports via <code>addImport()</code>. * * <p><a name="inner_types"> * <code>ImportManager</code> provides special handling for inner types, which are specified using <code>$</code> * instead of dot in the qualified name. Note that this means that <code>ImportManager</code> does <em>not</em> support * the use of <code>$</code> as a character within a class name, although this is allowed by the Java language. For * example: * * <pre> * result.append(importManager.getImportedName("com.example.lib.Outer$Inner", true));</pre> * * <p>This imports <code>com.example.lib.Outer</code> and returns <code>Outer.Inner</code>. Multiple levels of nested * classes are supported, but <code>$</code> <em>must</em> be used as the separator between all of them. A qualified * name with any dots following the first <code>$</code> is not allowed. * * <p>Note that it is also possible, instead, to use the simple dot notation for an Outer class, in which case the * containing class names will be treated as part of the package name and a more specific import will result. For * example: * * <pre> * result.append(importManager.getImportedName("com.example.lib.Outer.Inner", true));</pre> * * <p>This imports <code>com.example.lib.Outer.Inner</code> and returns <code>Inner</code>. Later, if the <code>$</code> * notation is used, it can match an import of this type: * * <pre> * result.append(importManager.getImportedName("com.example.lib.Outer$Inner", false));</pre> * * <p>This will use the existing import and return <code>Inner</code>. If, however, the second argument had been * <code>true</code> it would have imported <code>Outer</code> before getting the chance to consider <code>Inner</code>. * * <p><code>ImportManager</code> also handles array types, by always stripping off the indices (the square brackets) * when adding imports and preserving them when forming an imported name. * * @since 2.1 */ public class ImportManager { /** * The set of imports to be added to the compilation unit. */ protected SortedSet<String> imports = new TreeSet<String>(); /** * The mapping from short names to qualified names for explicit and implicit imports. */ protected HashMap<String, String> shortNameToImportMap = new HashMap<String, String>(); /** * The set of packages that have been imported with wildcards. * This can also include containing classes, when inner classes are registered using dot separators. */ protected HashSet<String> importedPackages; /* * The line delimiter string to use, or {@link System#getProperty(String) System.getProperty}<code>("line.separator")</code> * if null. * @since 2.5 */ private String lineDelimiter; /* * The string builder into which imports should be inserted. * @since 2.5 */ private StringBuilder importStringBuilder; /* * The string buffer into which imports should be inserted. * @since 2.5 */ private StringBuffer importStringBuffer; /* * The point in the string builder or buffer at which imports should be inserted. * @since 2.5 */ private int importInsertionPoint; /** * The set of short names from <code>java.lang</code> for which explicit import declarations are desired. */ protected HashSet<String> javaLangImports = null; /** * Creates an import manager for the given compilation unit package and short name. * This is the preferred constructor form, as it automatically adds the {@link #addMasterImport(String, String) master import} * for the compilation unit. * @since 2.5 * @see ImportManager#addMasterImport(String, String) */ public ImportManager(String compilationUnitPackage, String compilationUnitShortName) { this(compilationUnitPackage); addMasterImport(compilationUnitPackage, compilationUnitShortName); } /** * Creates an import manager for a compilation unit in the given package. * Note that the {@link #ImportManager(String, String) two-argument form} is preferred. * @see #ImportManager(String, String) */ public ImportManager(String compilationUnitPackage) { importedPackages = new HashSet<String>(); importedPackages.add(collapse(compilationUnitPackage)); } /* * Removes the whitespace from the given string if there is any. * The string is returned unchanged otherwise. */ private String collapse(String s) { char[] src = s.toCharArray(); char[] result = null; int srcLength = src.length; int resultLenth = -1; for (int i = 0; i < srcLength; i++) { if (Character.isWhitespace(src[i])) { if (result == null) { result = new char[srcLength]; System.arraycopy(src, 0, result, 0, i); resultLenth = i; } } else if (result != null) { result[resultLenth++] = src[i]; } } return result != null ? new String(result, 0, resultLenth) : s; } /* * Normalizes the given qualified name, package name, or short name. * This must be done, at some point, on any name that is recorded. */ private String normalize(String name) { int j = name.indexOf('['); return collapse(j == -1 ? name : name.substring(0, j)); } /* * Computes the normalized import name (package name + short name) from the given qualified name. */ private String getImportName(String qualifiedName) { int j = qualifiedName.indexOf('$'); if (j == -1) { j = qualifiedName.indexOf('['); } return collapse(j == -1 ? qualifiedName : qualifiedName.substring(0, j)); } /* * Computes the normalized package name from the given qualified or import name. */ private String getPackageName(String qualifiedName) { int j = qualifiedName.lastIndexOf('.'); return j == -1 ? "" : collapse(qualifiedName.substring(0, j)); } /* * Computes the normalized short name from the given qualified or import name. */ private String getShortName(String qualifiedName) { int i = qualifiedName.lastIndexOf('.') + 1; int j = qualifiedName.indexOf('$', i); if (j == -1) { j = qualifiedName.indexOf('[', i); } if (j == -1) { j = qualifiedName.length(); } return collapse(qualifiedName.substring(i, j)); } /* * Computes the raw name, which is suitable for use in code, from the given qualified name. * This is exactly the same name, but with <code>$</code> replaced by dot. */ private String getRawName(String qualifiedName) { return qualifiedName.replace('$', '.'); } /* * Computes the base name, which is suitable for use in code, from the given qualified name. * The base name is not normalized. */ private String getBaseName(String qualifiedName) { int i = qualifiedName.lastIndexOf('.') + 1; return qualifiedName.substring(i).replace('$', '.'); } /** * Returns the equivalent imported short name for the given qualified name, if there is one, or the qualified name * itself otherwise. Optionally, the qualified name can be automatically imported if possible. * In fact, a parameterized type expression is also allowed, in which case the expression will be parsed to obtain the * individual imported names within it. Then the full expression, with the appropriate substitutions, is returned. * @param qualifiedName the qualified name or parameterized type expression. * @param autoImport whether to try to automatically import types as needed. * @return the equivalent type or type expression, using short names wherever possible. * @since 2.5 */ public String getImportedName(String qualifiedName, boolean autoImport) { int i = qualifiedName.indexOf('<'); if (i == -1) { return basicGetImportedName(qualifiedName, autoImport); } StringBuilder result = new StringBuilder(); for (int start = 0, end = qualifiedName.length(); i < end; i++) { char c = qualifiedName.charAt(i); switch (c) { case ',': case '<': case '>': case '&': { if (start != i) { result.append(basicGetImportedName(qualifiedName.substring(start, i), autoImport)); } result.append(c); start = i + 1; break; } case '?': { int j = i + 1; while (j < end && Character.isWhitespace(qualifiedName.charAt(j))) { j++; } if (j + 6 < end && "extends".equals(qualifiedName.substring(j, j + 7))) { result.append(qualifiedName.substring(i, j + 7)); i = j + 6; } else if (j + 4 < end && "super".equals(qualifiedName.substring(j, j + 5))) { result.append(qualifiedName.substring(i, j + 5)); i = j + 4; } else { result.append(c); } start = i + 1; } default: { if (Character.isWhitespace(c) && start == i) { result.append(c); start++; } break; } } } return result.toString(); } /** * Returns the equivalent imported short name for the given qualified name, if there is one, or the qualified name * itself otherwise. Optionally, the qualified name can be automatically imported if possible. * In fact, a parameterized type expression is also allowed, in which case the expression will be parsed to obtain the * individual imported names within it. Then the full expression, with the appropriate substitutions, is returned. * @param qualifiedName the qualified name or parameterized type expression. * @param autoImport whether to try to automatically import types as needed. * @return the equivalent type or type expression, using short names wherever possible. */ public String getImportedName(String qualifiedName) { return getImportedName(qualifiedName, false); } /** * Gets the imported name for a single qualified name, optionally automatically importing if possible. */ protected String basicGetImportedName(String qualifiedName, boolean autoImport) { int i = qualifiedName.lastIndexOf('.'); if (i == -1) { return qualifiedName; } if (autoImport) { addImport(qualifiedName); } // For inner classes, we should try for an import at any level of nested class. // String rawName = getRawName(qualifiedName); char[] qualifiedNameChars = null; while (i != -1) { String baseName = getBaseName(qualifiedName); String importName = getImportName(qualifiedName); String shortName = getShortName(importName); String registeredName = shortNameToImportMap.get(shortName); if (registeredName == null) { // If no match, check for implicit java.lang import, but only on the first try (at the compilation unit level). // Failing that, check for matching package. // if (qualifiedNameChars == null && importName.equals("java.lang." + shortName)) { if (javaLangImports != null && javaLangImports.contains(shortName)) { imports.add(importName); } return baseName; } else if (importedPackages.contains(getPackageName(importName)) && (javaLangImports == null || !javaLangImports.contains(baseName))) { return baseName; } } else if (importName.equals(registeredName)) { return baseName; } // Convert the next $ to a dot. // i = qualifiedName.indexOf('$', i); if (i != -1) { if (qualifiedNameChars == null) { qualifiedNameChars = qualifiedName.toCharArray(); } qualifiedNameChars[i] = '.'; qualifiedName = new String(qualifiedNameChars); } } return rawName; } /** * Registers an import for the given package name and short name. * Note that the <code>$</code> notation for inner classes is <em>not</em> supported by this method, since the short * name is explicitly specified. * @param packageName the package name of the type to import * @param shortName the short name of the type to import, or <code>"*"</code> for a wildcard import. */ public void addImport(String packageName, String shortName) { basicAddImport(normalize(packageName), normalize(shortName), null); } /** * Registers an import for the given qualified name. * @param qualifiedName the qualified name of the type to import, which may end with <code>".*"</code> for a wildcard import. */ public void addImport(String qualifiedName) { String importName = getImportName(qualifiedName); basicAddImport(getPackageName(importName), getShortName(importName), importName); } /* * Adds an import for the given package name, short name, and import name, which must all have been normalized already. * The import name is actually optional: if null, it will be computed from the package name and short name. */ private void basicAddImport(String packageName, String shortName, String importName) { if (importName == null) { importName = new StringBuilder(packageName).append('.').append(shortName).toString(); } if (shortName.equals("*")) { importedPackages.add(packageName); imports.add(importName); } else if (!shortNameToImportMap.containsKey(shortName) && shouldImport(packageName, shortName, importName)) { shortNameToImportMap.put(shortName, importName); if (!importedPackages.contains(packageName)) { imports.add(importName); } } } /** * Determines whether the given non-wildcard import should be added. * By default, this returns false if the short name is a built-in Java language type name. * @since 2.8 */ protected boolean shouldImport(String packageName, String shortName, String importName) { return !CodeGenUtil.isJavaDefaultType(shortName); } /** * Registers a pseudo-import for the given qualified name. * A psuedo-import reserves the mapping from short name to qualified name, just like an ordinary import, but does not * actually include the import declarations among those returned by {@link #computeSortedImports()} or emitted by * {@link #emitSortedImports()}. * Note that all pseudo-imports must be added before any other ordinary imports. * @param qualifiedName the qualified name of the type to import, which may end with <code>".*"</code> for a wildcard import. * @see #computeSortedImports() * @see #emitSortedImports() */ public void addPseudoImport(String qualifiedName) { String importName = getImportName(qualifiedName); String shortName = getShortName(importName); if (shortName.equals("*")) { String packageName = getPackageName(importName); importedPackages.add(packageName); } else { shortNameToImportMap.put(shortName, importName); } } /** * Reserves the import mapping for the given package and short name of the compilation unit. * The <code>$</code> notation for inner classes is <em>not</em> supported by this method, since the short name is * explicitly specified. * Note that a master import must be added before any pseudo- or ordinary imports. However, it need not be done * explicitly if the preferred, {@link #ImportManager(String, String) two-argument constructor} form is used. * @see #ImportManager(String, String) */ public void addMasterImport(String packageName, String shortName) { packageName = normalize(packageName); shortName = normalize(shortName); shortNameToImportMap.put(shortName, new StringBuilder(packageName).append('.').append(shortName).toString()); } /** * Returns whether a mapping for the given short name has been reserved. */ public boolean hasImport(String shortName) { return shortNameToImportMap.containsKey(normalize(shortName)); } /** * Returns the list of qualified names for which imports are to be added to the compilation unit. */ public Collection<String> getImports() { compactImports(); return imports; } /* * Removes any explicit imports that are already covered by wildcards. */ private void compactImports() { for (Iterator<String> i = imports.iterator(); i.hasNext(); ) { String importName = i.next(); if (!getShortName(importName).equals("*") && importedPackages.contains(getPackageName(importName))) { i.remove(); } } } /** * Returns the line delimiter to be used in {@link #computeSortedImports()}. * By default, this is {@link System#getProperty(String) System.getProperty}<code>("line.separator")</code>. * @see #computeSortedImports() * @see #setLineDelimiter(String) * @since 2.5 */ public String getLineDelimiter() { return lineDelimiter == null ? System.getProperty("line.separator") : lineDelimiter; } /** * Sets the line delimiter to be used in {@link #computeSortedImports()}. * @see #computeSortedImports() * @see #getLineDelimiter() * @since 2.5 */ public void setLineDelimiter(String lineDelimiter) { this.lineDelimiter = lineDelimiter; } /** * Returns the sorted, formatted import declarations that should be added to the compilation unit. * Each statement appears on its own line, with an additional {@link #getLineDelimiter() line delimiter} before the * first import and between imports from different packages. * @see #getLineDelimiter() */ public String computeSortedImports() { String NL = getLineDelimiter(); String previousPackageName = null; StringBuffer result = new StringBuffer(); for (String importName : getImports()) { String packageName = getPackageName(importName); if (previousPackageName != null && !previousPackageName.equals(packageName)) { result.append(NL); } previousPackageName = packageName; result.append(NL + "import " + importName + ";"); } return result.toString(); } /** * Registers {@link #addPseudoImport(String) pseudo-imports} for all of the import declarations in the specified * compilation unit contents. * This uses JDT's Java AST API to parse the code when Eclipse is running, and a simpler, less accurate approach * based on regular expressions otherwise. * Note that this must be invoked before any ordinary imports are added. * @param compilationUnitContents the contents of a Java source file. * @see #addPseudoImport(String) */ public void addCompilationUnitImports(String compilationUnitContents) { List<String> imports = EMFPlugin.IS_ECLIPSE_RUNNING ? EclipseHelper.getCompilationUnitImports(compilationUnitContents) : getCompilationUnitImports(compilationUnitContents); for (String qualifiedName : imports) { addPseudoImport(qualifiedName); } } /* * Uses a regular expression to try to parse the import statements from the given compilation unit contents. */ private List<String> getCompilationUnitImports(String compilationUnitContents) { List<String> result = new ArrayList<String>(); Pattern importPattern = Pattern.compile("import\\s+([^\\s;]*);\\s*", Pattern.MULTILINE | Pattern.DOTALL); Matcher matcher = importPattern.matcher(compilationUnitContents); while (matcher.find()) { result.add(matcher.group(1)); } return result; } /* * Uses JDT's AST API to parse the import statements from the given compilation unit contents. */ private static class EclipseHelper { public static List<String> getCompilationUnitImports(String compilationUnitContents) { List<String> result = new ArrayList<String>(); ASTParser parser = CodeGenUtil.EclipseUtil.newASTParser(); parser.setSource(compilationUnitContents.toCharArray()); CompilationUnit compilationUnit = (CompilationUnit)parser.createAST(new NullProgressMonitor()); for (Iterator<?> i = compilationUnit.imports().iterator(); i.hasNext();) { ImportDeclaration importDeclaration = (ImportDeclaration)i.next(); result.add(importDeclaration.getName().getFullyQualifiedName()); } return result; } } /** * Records the given <code>StringBuilder</code> and its current length, so that computed imports can later be * {@link #emitSortedImports() emitted}, and {@link #addCompilationUnitImports(String) adds} any import declarations * that the builder already contains. * @since 2.5 * @see #emitSortedImports() * @see #addCompilationUnitImports(String) */ public void markImportLocation(StringBuilder stringBuilder) { importStringBuffer = null; importStringBuilder = stringBuilder; importInsertionPoint = stringBuilder.length(); addCompilationUnitImports(stringBuilder.toString()); } /** * Records the given <code>StringBuffer</code> and its current length, so that computed imports can later be * {@link #emitSortedImports() emitted}, and {@link #addCompilationUnitImports(String) adds} any import declarations * that the buffer already contains. * @since 2.5 * @see #emitSortedImports() * @see #addCompilationUnitImports(String) */ public void markImportLocation(StringBuffer stringBuffer) { importStringBuilder = null; importStringBuffer = stringBuffer; importInsertionPoint = stringBuffer.length(); addCompilationUnitImports(stringBuffer.toString()); } /** * Inserts all the {@link #computeSortedImports() computed imports} for the compilation unit into the recorded * {@link #markImportLocation(StringBuilder) StringBuilder} or {@link #markImportLocation(StringBuffer) StringBuffer}. * @since 2.5 * @see #computeSortedImports() * @see #markImportLocation(StringBuilder) * @see #markImportLocation(StringBuffer) */ public void emitSortedImports() { if (importStringBuilder != null) { importStringBuilder.insert(importInsertionPoint, computeSortedImports()); } else if (importStringBuffer != null) { importStringBuffer.insert(importInsertionPoint, computeSortedImports()); } } /** * Ensures that explicit import declarations will be added for classes from <code>java.lang</code> with the * specified short names. * By default, all the short names of classes in this package are reserved so that the implicit imports are used. * By specifying particular classes with this method, those imports, if actually used, will be made explicit. * @param javaLangClassNames */ public void addJavaLangImports(List<String> javaLangClassNames) { if (!javaLangClassNames.isEmpty()) { javaLangImports = new HashSet<String>(); for (String shortName : javaLangClassNames) { javaLangImports.add(normalize(shortName)); } } } }