| /******************************************************************************* |
| * Copyright (c) 2009, 2010 Oracle. 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: |
| * Oracle - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.jpt.common.core.utility; |
| |
| import java.io.Serializable; |
| import java.io.StringWriter; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.TreeSet; |
| |
| import org.eclipse.jpt.common.utility.Filter; |
| import org.eclipse.jpt.common.utility.IndentingPrintWriter; |
| import org.eclipse.jpt.common.utility.internal.CollectionTools; |
| import org.eclipse.jpt.common.utility.internal.StringTools; |
| import org.eclipse.jpt.common.utility.internal.Transformer; |
| import org.eclipse.jpt.common.utility.internal.iterables.FilteringIterable; |
| import org.eclipse.jpt.common.utility.internal.iterables.TransformationIterable; |
| |
| import com.ibm.icu.text.Collator; |
| |
| /** |
| * Extend {@link IndentingPrintWriter} with some methods that facilitate |
| * building class source code. |
| */ |
| @SuppressWarnings("nls") |
| public class BodySourceWriter |
| extends IndentingPrintWriter |
| { |
| protected final String packageName; |
| protected final String className; |
| // key = short class name; value = import package |
| protected final HashMap<String, ImportPackage> imports = new HashMap<String, ImportPackage>(); |
| |
| public BodySourceWriter(String packageName, String className) { |
| super(new StringWriter(2000)); |
| this.packageName = packageName; |
| this.className = className; |
| } |
| |
| public String getSource() { |
| return this.out.toString(); |
| } |
| |
| public int getLength() { |
| return ((StringWriter) this.out).getBuffer().length(); |
| } |
| |
| protected void printVisibility(String visibilityModifier) { |
| if (visibilityModifier.length() != 0) { |
| this.print(visibilityModifier); |
| this.print(' '); |
| } |
| } |
| |
| public void printAnnotation(String annotationName) { |
| this.print('@'); |
| this.printTypeDeclaration(annotationName); |
| } |
| |
| public void printTypeDeclaration(String typeDeclaration) { |
| this.print(this.buildImportedTypeDeclaration(typeDeclaration)); |
| } |
| |
| protected void printField(String fieldName, String typeDeclaration, String visibility) { |
| this.printVisibility(visibility); |
| this.printTypeDeclaration(typeDeclaration); |
| this.print(' '); |
| this.print(fieldName); |
| this.print(';'); |
| this.println(); |
| this.println(); |
| } |
| |
| protected void printParameterizedField(String fieldName, String typeDeclaration, String parameterTypeDeclaration, String visibility) { |
| this.printVisibility(visibility); |
| this.printTypeDeclaration(typeDeclaration); |
| this.print('<'); |
| this.printTypeDeclaration(parameterTypeDeclaration); |
| this.print('>'); |
| this.print(' '); |
| this.print(fieldName); |
| this.print(';'); |
| this.println(); |
| this.println(); |
| } |
| |
| /** |
| * Convert the specified string to a <em>String Literal</em> and print it, |
| * adding the surrounding double-quotes and escaping characters |
| * as necessary. |
| */ |
| public void printStringLiteral(String string) { |
| StringTools.convertToJavaStringLiteralOn(string, this); |
| } |
| |
| |
| // ********** imports ********** |
| |
| // ***** writing |
| /** |
| * Return the specified class's "imported" name. |
| * The class declaration must be of the form: |
| * "int" |
| * "int[]" (not "[I") |
| * "java.lang.Object" |
| * "java.lang.Object[]" (not "[Ljava.lang.Object;") |
| * "java.util.Map.Entry" (not "java.util.Map$Entry") |
| * "java.util.Map.Entry[][]" (not "[[Ljava.util.Map$Entry;") |
| * |
| * To really do this right, we would need to gather all the types from |
| * the "unamed" (default) package that were referenced in the |
| * compilation unit beforehand. *Any* collisions with one of these |
| * types would have to be fully qualified (whether it was from |
| * 'java.lang' or the same package as the current compilation unit). |
| * In other words, if we have any types from the "unnamed" package, |
| * results are unpredictable.... |
| */ |
| protected String buildImportedTypeDeclaration(String typeDeclaration) { |
| if (this.typeDeclarationIsMemberClass(typeDeclaration)) { |
| // no need for an import, just return the partially-qualified name |
| return this.buildMemberClassTypeDeclaration(typeDeclaration); |
| } |
| int last = typeDeclaration.lastIndexOf('.'); |
| String currentPackageName = (last == -1) ? "" : typeDeclaration.substring(0, last); |
| String shortTypeDeclaration = typeDeclaration.substring(last + 1); |
| String shortElementTypeName = shortTypeDeclaration; |
| while (shortElementTypeName.endsWith("[]")) { |
| shortElementTypeName = shortElementTypeName.substring(0, shortElementTypeName.length() - 2); |
| } |
| ImportPackage prev = this.imports.get(shortElementTypeName); |
| if (prev == null) { |
| // this is the first class with this short element type name |
| this.imports.put(shortElementTypeName, new ImportPackage(currentPackageName)); |
| return shortTypeDeclaration; |
| } |
| if (prev.packageName.equals(currentPackageName)) { |
| // this element type has already been imported |
| return shortTypeDeclaration; |
| } |
| if (currentPackageName.equals(this.packageName) && |
| prev.packageName.equals("java.lang")) { |
| // we force the 'java.lang' class to be explicitly imported |
| prev.collision = true; |
| } |
| // another class with the same short element type name has been |
| // previously imported, so this one must be used fully-qualified |
| return typeDeclaration; |
| } |
| |
| /** |
| * e.g. "foo.bar.Employee.PK" will return true |
| */ |
| protected boolean typeDeclarationIsMemberClass(String typeDeclaration) { |
| return (typeDeclaration.length() > this.className.length()) |
| && typeDeclaration.startsWith(this.className) |
| && (typeDeclaration.charAt(this.className.length()) == '.'); |
| } |
| |
| /** |
| * e.g. "foo.bar.Employee.PK" will return "Employee.PK" |
| * this prevents collisions with other imported classes (e.g. "joo.jar.PK") |
| */ |
| protected String buildMemberClassTypeDeclaration(String typeDeclaration) { |
| int index = this.packageName.length(); |
| if (index != 0) { |
| index++; // bump past the '.' |
| } |
| return typeDeclaration.substring(index); |
| } |
| |
| // ***** reading |
| public Iterable<String> getImports() { |
| return this.getSortedRequiredImports(); |
| } |
| |
| /** |
| * transform our map entries to class names |
| */ |
| protected Iterable<String> getSortedRequiredImports() { |
| return new TransformationIterable<Map.Entry<String, ImportPackage>, String>(this.getSortedRequiredImportEntries(), this.buildImportEntriesTransformer()); |
| } |
| |
| protected Transformer<Map.Entry<String, ImportPackage>, String> buildImportEntriesTransformer() { |
| return IMPORT_ENTRIES_TRANSFORMER; |
| } |
| |
| protected static final Transformer<Map.Entry<String, ImportPackage>, String> IMPORT_ENTRIES_TRANSFORMER = new ImportEntriesTransformer(); |
| |
| protected static class ImportEntriesTransformer |
| implements Transformer<Map.Entry<String, ImportPackage>, String> |
| { |
| public String transform(Entry<String, ImportPackage> importEntry) { |
| String pkg = importEntry.getValue().packageName; |
| String type = importEntry.getKey(); |
| StringBuilder sb = new StringBuilder(pkg.length() + 1 + type.length()); |
| sb.append(pkg); |
| sb.append('.'); |
| sb.append(type); |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * sort by package first, then class (*not* by fully-qualified class name) |
| */ |
| protected Iterable<Map.Entry<String, ImportPackage>> getSortedRequiredImportEntries() { |
| TreeSet<Map.Entry<String, ImportPackage>> sortedEntries = new TreeSet<Map.Entry<String, ImportPackage>>(this.buildImportEntriesComparator()); |
| CollectionTools.addAll(sortedEntries, this.getRequiredImportEntries()); |
| return sortedEntries; |
| } |
| |
| protected Comparator<Map.Entry<String, ImportPackage>> buildImportEntriesComparator() { |
| return IMPORT_ENTRIES_COMPARATOR; |
| } |
| |
| protected static final Comparator<Map.Entry<String, ImportPackage>> IMPORT_ENTRIES_COMPARATOR = new ImportEntriesComparator(); |
| |
| protected static class ImportEntriesComparator |
| implements Comparator<Map.Entry<String, ImportPackage>>, Serializable |
| { |
| public int compare(Map.Entry<String, ImportPackage> e1, Map.Entry<String, ImportPackage> e2) { |
| Collator collator = Collator.getInstance(); |
| int pkg = collator.compare(e1.getValue().packageName, e2.getValue().packageName); |
| return (pkg == 0) ? collator.compare(e1.getKey(), e2.getKey()) : pkg; |
| } |
| } |
| |
| /** |
| * strip off any non-required imports (e.g. "java.lang.Object') |
| */ |
| protected Iterable<Map.Entry<String, ImportPackage>> getRequiredImportEntries() { |
| return new FilteringIterable<Map.Entry<String, ImportPackage>>(this.imports.entrySet(), this.buildRequiredImportEntriesFilter()); |
| } |
| |
| protected Filter<Map.Entry<String, ImportPackage>> buildRequiredImportEntriesFilter() { |
| return new RequiredImportEntriesFilter(); |
| } |
| |
| protected class RequiredImportEntriesFilter |
| implements Filter<Map.Entry<String, ImportPackage>> |
| { |
| public boolean accept(Map.Entry<String, ImportPackage> importEntry) { |
| return this.packageMustBeImported(importEntry.getValue()); |
| } |
| |
| protected boolean packageMustBeImported(ImportPackage importPackage) { |
| String pkg = importPackage.packageName; |
| if (pkg.equals("")) { |
| // cannot import a type from the "unnamed" package |
| return false; |
| } |
| if (pkg.equals("java.lang")) { |
| // we must import from 'java.lang' if we also have a class in the same package |
| return importPackage.collision; |
| } |
| if (pkg.equals(BodySourceWriter.this.packageName)) { |
| // we never need to import a class from the same package |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * We need a 'collision' flag for when we encounter a class from |
| * 'java.lang' followed by a class from the current compilation unit's |
| * package. We will need to include the explicit import of the |
| * 'java.lang' class and all the references to the other class will |
| * have to be fully-qualified. |
| * |
| * If the classes are encountered in the opposite order (i.e. the class |
| * from the current compilation unit's package followed by the class |
| * from 'java.lang'), we do *not* need to import the first class while |
| * all the references to the 'java.lang' class will be fully-qualified. |
| * |
| * Unfortunately, we still have a problem: if we reference a class from |
| * 'java.lang' and there is a conflicting class from the current |
| * compilation unit's package (but that class is *not* revealed to us |
| * here), the simple name will be resolved to the non-'java.lang' class. |
| * Unless we simply force an import of *all* 'java.lang' classes.... :-( |
| * |
| * This shouldn't happen very often. :-) |
| */ |
| protected static class ImportPackage { |
| protected final String packageName; |
| protected boolean collision = false; |
| |
| protected ImportPackage(String packageName) { |
| super(); |
| this.packageName = packageName; |
| } |
| } |
| |
| } |