Skip to main content
summaryrefslogtreecommitdiffstats
blob: 9aabeebdd5da8c45b2d99303369c1b31625cba60 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*******************************************************************************
 * 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;
		}
	}

}

Back to the top