blob: 0be7b18053877af77a52480b48582031712a9846 [file] [log] [blame]
/*******************************************************************************
* 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.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.utility.Filter;
import org.eclipse.jpt.utility.IndentingPrintWriter;
import org.eclipse.jpt.utility.internal.CollectionTools;
import org.eclipse.jpt.utility.internal.StringTools;
import org.eclipse.jpt.utility.internal.Transformer;
import org.eclipse.jpt.utility.internal.iterables.FilteringIterable;
import org.eclipse.jpt.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;
}
}
}