blob: 078c3f29a08b81d558ccc6e6e7dec00655a454a7 [file] [log] [blame]
// Copyright 2013 Square, Inc.
package com.squareup.javawriter;
import static javax.lang.model.element.Modifier.ABSTRACT;
import java.io.Closeable;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.Modifier;
/** A utility class which aids in generating Java source files. */
public class JavaWriter implements Closeable {
private static final Pattern TYPE_PATTERN = Pattern.compile("(?:[\\w$]+\\.)*([\\w\\.*$]+)");
private static final int MAX_SINGLE_LINE_ATTRIBUTES = 3;
private static final String INDENT = " ";
/** Map fully qualified type names to their short names. */
private final Map<String, String> importedTypes = new LinkedHashMap<String, String>();
private String packagePrefix;
private final Deque<Scope> scopes = new ArrayDeque<Scope>();
private final Deque<String> types = new ArrayDeque<String>();
private final Writer out;
private boolean isCompressingTypes = true;
private String indent = INDENT;
/**
* @param out the stream to which Java source will be written. This should be a buffered stream.
*/
public JavaWriter(Writer out) {
this.out = out;
}
public void setCompressingTypes(boolean isCompressingTypes) {
this.isCompressingTypes = isCompressingTypes;
}
public boolean isCompressingTypes() {
return isCompressingTypes;
}
public void setIndent(String indent) {
this.indent = indent;
}
public String getIndent() {
return indent;
}
/** Emit a package declaration and empty line. */
public JavaWriter emitPackage(String packageName) throws IOException {
if (this.packagePrefix != null) {
throw new IllegalStateException();
}
if (packageName.isEmpty()) {
this.packagePrefix = "";
} else {
out.write("package ");
out.write(packageName);
out.write(";\n\n");
this.packagePrefix = packageName + ".";
}
return this;
}
/**
* Emit an import for each {@code type} provided. For the duration of the file, all references to
* these classes will be automatically shortened.
*/
public JavaWriter emitImports(String... types) throws IOException {
return emitImports(Arrays.asList(types));
}
/**
* Emit an import for each {@code type} in the provided {@code Collection}. For the duration of
* the file, all references to these classes will be automatically shortened.
*/
public JavaWriter emitImports(Collection<String> types) throws IOException {
for (String type : new TreeSet<String>(types)) {
Matcher matcher = TYPE_PATTERN.matcher(type);
if (!matcher.matches()) {
throw new IllegalArgumentException(type);
}
if (importedTypes.put(type, matcher.group(1)) != null) {
throw new IllegalArgumentException(type);
}
out.write("import ");
out.write(type);
out.write(";\n");
}
return this;
}
/**
* Emit a static import for each {@code type} provided. For the duration of the file,
* all references to these classes will be automatically shortened.
*/
public JavaWriter emitStaticImports(String... types) throws IOException {
return emitStaticImports(Arrays.asList(types));
}
/**
* Emit a static import for each {@code type} in the provided {@code Collection}. For the
* duration of the file, all references to these classes will be automatically shortened.
*/
public JavaWriter emitStaticImports(Collection<String> types) throws IOException {
for (String type : new TreeSet<String>(types)) {
Matcher matcher = TYPE_PATTERN.matcher(type);
if (!matcher.matches()) {
throw new IllegalArgumentException(type);
}
if (importedTypes.put(type, matcher.group(1)) != null) {
throw new IllegalArgumentException(type);
}
out.write("import static ");
out.write(type);
out.write(";\n");
}
return this;
}
/**
* Emits a name like {@code java.lang.String} or {@code java.util.List<java.lang.String>},
* compressing it with imports if possible. Type compression will only be enabled if
* {@link #isCompressingTypes} is true.
*/
private JavaWriter emitCompressedType(String type) throws IOException {
if (isCompressingTypes) {
out.write(compressType(type));
} else {
out.write(type);
}
return this;
}
/** Try to compress a fully-qualified class name to only the class name. */
public String compressType(String type) {
StringBuilder sb = new StringBuilder();
if (this.packagePrefix == null) {
throw new IllegalStateException();
}
Matcher m = TYPE_PATTERN.matcher(type);
int pos = 0;
while (true) {
boolean found = m.find(pos);
// Copy non-matching characters like "<".
int typeStart = found ? m.start() : type.length();
sb.append(type, pos, typeStart);
if (!found) {
break;
}
// Copy a single class name, shortening it if possible.
String name = m.group(0);
String imported = importedTypes.get(name);
if (imported != null) {
sb.append(imported);
} else if (isClassInPackage(name)) {
String compressed = name.substring(packagePrefix.length());
if (isAmbiguous(compressed)) {
sb.append(name);
} else {
sb.append(compressed);
}
} else if (name.startsWith("java.lang.")) {
sb.append(name.substring("java.lang.".length()));
} else {
sb.append(name);
}
pos = m.end();
}
return sb.toString();
}
private boolean isClassInPackage(String name) {
if (name.startsWith(packagePrefix)) {
if (name.indexOf('.', packagePrefix.length()) == -1) {
return true;
}
int index = name.indexOf('.');
if (name.substring(index + 1, index + 2).matches("[A-Z]")) {
return true;
}
}
return false;
}
/**
* Returns true if the imports contain a class with same simple name as {@code compressed}.
*
* @param compressed simple name of the type
*/
private boolean isAmbiguous(String compressed) {
return importedTypes.values().contains(compressed);
}
/**
* Emits an initializer declaration.
*
* @param isStatic true if it should be an static initializer, false for an instance initializer.
*/
public JavaWriter beginInitializer(boolean isStatic) throws IOException {
indent();
if (isStatic) {
out.write("static");
out.write(" {\n");
} else {
out.write("{\n");
}
scopes.push(Scope.INITIALIZER);
return this;
}
/** Ends the current initializer declaration. */
public JavaWriter endInitializer() throws IOException {
popScope(Scope.INITIALIZER);
indent();
out.write("}\n");
return this;
}
/**
* Emits a type declaration.
*
* @param kind such as "class", "interface" or "enum".
*/
public JavaWriter beginType(String type, String kind) throws IOException {
return beginType(type, kind, EnumSet.noneOf(Modifier.class), null);
}
/**
* Emits a type declaration.
*
* @param kind such as "class", "interface" or "enum".
*/
public JavaWriter beginType(String type, String kind, Set<Modifier> modifiers)
throws IOException {
return beginType(type, kind, modifiers, null);
}
/**
* Emits a type declaration.
*
* @param kind such as "class", "interface" or "enum".
* @param extendsType the class to extend, or null for no extends clause.
*/
public JavaWriter beginType(String type, String kind, Set<Modifier> modifiers, String extendsType,
String... implementsTypes) throws IOException {
indent();
emitModifiers(modifiers);
out.write(kind);
out.write(" ");
emitCompressedType(type);
if (extendsType != null) {
out.write(" extends ");
emitCompressedType(extendsType);
}
if (implementsTypes.length > 0) {
out.write("\n");
indent();
out.write(" implements ");
for (int i = 0; i < implementsTypes.length; i++) {
if (i != 0) {
out.write(", ");
}
emitCompressedType(implementsTypes[i]);
}
}
out.write(" {\n");
scopes.push(Scope.TYPE_DECLARATION);
types.push(type);
return this;
}
/** Completes the current type declaration. */
public JavaWriter endType() throws IOException {
popScope(Scope.TYPE_DECLARATION);
types.pop();
indent();
out.write("}\n");
return this;
}
/** Emits a field declaration. */
public JavaWriter emitField(String type, String name) throws IOException {
return emitField(type, name, EnumSet.noneOf(Modifier.class), null);
}
/** Emits a field declaration. */
public JavaWriter emitField(String type, String name, Set<Modifier> modifiers)
throws IOException {
return emitField(type, name, modifiers, null);
}
public JavaWriter emitField(String type, String name, Set<Modifier> modifiers,
String initialValue) throws IOException {
indent();
emitModifiers(modifiers);
emitCompressedType(type);
out.write(" ");
out.write(name);
if (initialValue != null) {
out.write(" = ");
out.write(initialValue);
}
out.write(";\n");
return this;
}
/**
* Emit a method declaration.
*
* <p>A {@code null} return type may be used to indicate a constructor, but
* {@link #beginConstructor(Set, String...)} should be preferred. This behavior may be removed in
* a future release.
*
* @param returnType the method's return type, or null for constructors
* @param name the method name, or the fully qualified class name for constructors.
* @param modifiers the set of modifiers to be applied to the method
* @param parameters alternating parameter types and names.
*/
public JavaWriter beginMethod(String returnType, String name, Set<Modifier> modifiers,
String... parameters) throws IOException {
return beginMethod(returnType, name, modifiers, Arrays.asList(parameters), null);
}
/**
* Emit a method declaration.
*
* <p>A {@code null} return type may be used to indicate a constructor, but
* {@link #beginConstructor(Set, List, List)} should be preferred. This behavior may be removed in
* a future release.
*
* @param returnType the method's return type, or null for constructors.
* @param name the method name, or the fully qualified class name for constructors.
* @param modifiers the set of modifiers to be applied to the method
* @param parameters alternating parameter types and names.
* @param throwsTypes the classes to throw, or null for no throws clause.
*/
public JavaWriter beginMethod(String returnType, String name, Set<Modifier> modifiers,
List<String> parameters, List<String> throwsTypes) throws IOException {
indent();
emitModifiers(modifiers);
if (returnType != null) {
emitCompressedType(returnType);
out.write(" ");
out.write(name);
} else {
emitCompressedType(name);
}
out.write("(");
if (parameters != null) {
for (int p = 0; p < parameters.size();) {
if (p != 0) {
out.write(", ");
}
emitCompressedType(parameters.get(p++));
out.write(" ");
emitCompressedType(parameters.get(p++));
}
}
out.write(")");
if (throwsTypes != null && throwsTypes.size() > 0) {
out.write("\n");
indent();
out.write(" throws ");
for (int i = 0; i < throwsTypes.size(); i++) {
if (i != 0) {
out.write(", ");
}
emitCompressedType(throwsTypes.get(i));
}
}
if (modifiers.contains(ABSTRACT)) {
out.write(";\n");
scopes.push(Scope.ABSTRACT_METHOD);
} else {
out.write(" {\n");
scopes.push(returnType == null ? Scope.CONSTRUCTOR : Scope.NON_ABSTRACT_METHOD);
}
return this;
}
public JavaWriter beginConstructor(Set<Modifier> modifiers, String... parameters)
throws IOException {
beginMethod(null, types.peekFirst(), modifiers, parameters);
return this;
}
public JavaWriter beginConstructor(Set<Modifier> modifiers,
List<String> parameters, List<String> throwsTypes)
throws IOException {
beginMethod(null, types.peekFirst(), modifiers, parameters, throwsTypes);
return this;
}
/** Emits some Javadoc comments with line separated by {@code \n}. */
public JavaWriter emitJavadoc(String javadoc, Object... params) throws IOException {
String formatted = String.format(javadoc, params);
indent();
out.write("/**\n");
for (String line : formatted.split("\n")) {
indent();
out.write(" *");
if (!line.isEmpty()) {
out.write(" ");
out.write(line);
}
out.write("\n");
}
indent();
out.write(" */\n");
return this;
}
/** Emits a single line comment. */
public JavaWriter emitSingleLineComment(String comment, Object... args) throws IOException {
indent();
out.write("// ");
out.write(String.format(comment, args));
out.write("\n");
return this;
}
public JavaWriter emitEmptyLine() throws IOException {
out.write("\n");
return this;
}
public JavaWriter emitEnumValue(String name) throws IOException {
indent();
out.write(name);
out.write(",\n");
return this;
}
/** Equivalent to {@code annotation(annotation, emptyMap())}. */
public JavaWriter emitAnnotation(String annotation) throws IOException {
return emitAnnotation(annotation, Collections.<String, Object>emptyMap());
}
/** Equivalent to {@code annotation(annotationType.getName(), emptyMap())}. */
public JavaWriter emitAnnotation(Class<? extends Annotation> annotationType) throws IOException {
return emitAnnotation(type(annotationType), Collections.<String, Object>emptyMap());
}
/**
* Annotates the next element with {@code annotationType} and a {@code value}.
*
* @param value an object used as the default (value) parameter of the annotation. The value will
* be encoded using Object.toString(); use {@link #stringLiteral} for String values. Object
* arrays are written one element per line.
*/
public JavaWriter emitAnnotation(Class<? extends Annotation> annotationType, Object value)
throws IOException {
return emitAnnotation(type(annotationType), value);
}
/**
* Annotates the next element with {@code annotation} and a {@code value}.
*
* @param value an object used as the default (value) parameter of the annotation. The value will
* be encoded using Object.toString(); use {@link #stringLiteral} for String values. Object
* arrays are written one element per line.
*/
public JavaWriter emitAnnotation(String annotation, Object value) throws IOException {
indent();
out.write("@");
emitCompressedType(annotation);
out.write("(");
emitAnnotationValue(value);
out.write(")");
out.write("\n");
return this;
}
/** Equivalent to {@code annotation(annotationType.getName(), attributes)}. */
public JavaWriter emitAnnotation(Class<? extends Annotation> annotationType,
Map<String, ?> attributes) throws IOException {
return emitAnnotation(type(annotationType), attributes);
}
/**
* Annotates the next element with {@code annotation} and {@code attributes}.
*
* @param attributes a map from annotation attribute names to their values. Values are encoded
* using Object.toString(); use {@link #stringLiteral} for String values. Object arrays are
* written one element per line.
*/
public JavaWriter emitAnnotation(String annotation, Map<String, ?> attributes)
throws IOException {
indent();
out.write("@");
emitCompressedType(annotation);
switch (attributes.size()) {
case 0:
break;
case 1:
Entry<String, ?> onlyEntry = attributes.entrySet().iterator().next();
out.write("(");
if (!"value".equals(onlyEntry.getKey())) {
out.write(onlyEntry.getKey());
out.write(" = ");
}
emitAnnotationValue(onlyEntry.getValue());
out.write(")");
break;
default:
boolean split = attributes.size() > MAX_SINGLE_LINE_ATTRIBUTES
|| containsArray(attributes.values());
out.write("(");
scopes.push(Scope.ANNOTATION_ATTRIBUTE);
String separator = split ? "\n" : "";
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
out.write(separator);
separator = split ? ",\n" : ", ";
if (split) {
indent();
}
out.write(entry.getKey());
out.write(" = ");
Object value = entry.getValue();
emitAnnotationValue(value);
}
popScope(Scope.ANNOTATION_ATTRIBUTE);
if (split) {
out.write("\n");
indent();
}
out.write(")");
break;
}
out.write("\n");
return this;
}
private boolean containsArray(Collection<?> values) {
for (Object value : values) {
if (value instanceof Object[]) {
return true;
}
}
return false;
}
/**
* Writes a single annotation value. If the value is an array, each element in the array will be
* written to its own line.
*/
private JavaWriter emitAnnotationValue(Object value) throws IOException {
if (value instanceof Object[]) {
out.write("{");
boolean firstValue = true;
scopes.push(Scope.ANNOTATION_ARRAY_VALUE);
for (Object o : ((Object[]) value)) {
if (firstValue) {
firstValue = false;
out.write("\n");
} else {
out.write(",\n");
}
indent();
out.write(o.toString());
}
popScope(Scope.ANNOTATION_ARRAY_VALUE);
out.write("\n");
indent();
out.write("}");
} else {
out.write(value.toString());
}
return this;
}
/**
* @param pattern a code pattern like "int i = %s". Newlines will be further indented. Should not
* contain trailing semicolon.
*/
public JavaWriter emitStatement(String pattern, Object... args) throws IOException {
checkInMethod();
String[] lines = String.format(pattern, args).split("\n", -1);
indent();
out.write(lines[0]);
for (int i = 1; i < lines.length; i++) {
out.write("\n");
hangingIndent();
out.write(lines[i]);
}
out.write(";\n");
return this;
}
/**
* @param controlFlow the control flow construct and its code, such as "if (foo == 5)". Shouldn't
* contain braces or newline characters.
*/
public JavaWriter beginControlFlow(String controlFlow) throws IOException {
checkInMethod();
indent();
out.write(controlFlow);
out.write(" {\n");
scopes.push(Scope.CONTROL_FLOW);
return this;
}
/**
* @param controlFlow the control flow construct and its code, such as "else if (foo == 10)".
* Shouldn't contain braces or newline characters.
*/
public JavaWriter nextControlFlow(String controlFlow) throws IOException {
popScope(Scope.CONTROL_FLOW);
indent();
scopes.push(Scope.CONTROL_FLOW);
out.write("} ");
out.write(controlFlow);
out.write(" {\n");
return this;
}
public JavaWriter endControlFlow() throws IOException {
return endControlFlow(null);
}
/**
* @param controlFlow the optional control flow construct and its code, such as
* "while(foo == 20)". Only used for "do/while" control flows.
*/
public JavaWriter endControlFlow(String controlFlow) throws IOException {
popScope(Scope.CONTROL_FLOW);
indent();
if (controlFlow != null) {
out.write("} ");
out.write(controlFlow);
out.write(";\n");
} else {
out.write("}\n");
}
return this;
}
/** Completes the current method declaration. */
public JavaWriter endMethod() throws IOException {
Scope popped = scopes.pop();
// support calling a constructor a "method" to support the legacy code
if (popped == Scope.NON_ABSTRACT_METHOD || popped == Scope.CONSTRUCTOR) {
indent();
out.write("}\n");
} else if (popped != Scope.ABSTRACT_METHOD) {
throw new IllegalStateException();
}
return this;
}
/** Completes the current constructor declaration. */
public JavaWriter endConstructor() throws IOException {
popScope(Scope.CONSTRUCTOR);
indent();
out.write("}\n");
return this;
}
/** Returns the string literal representing {@code data}, including wrapping quotes. */
public static String stringLiteral(String data) {
StringBuilder result = new StringBuilder();
result.append('"');
for (int i = 0; i < data.length(); i++) {
char c = data.charAt(i);
switch (c) {
case '"':
result.append("\\\"");
break;
case '\\':
result.append("\\\\");
break;
case '\b':
result.append("\\b");
break;
case '\t':
result.append("\\t");
break;
case '\n':
result.append("\\n");
break;
case '\f':
result.append("\\f");
break;
case '\r':
result.append("\\r");
break;
default:
if (Character.isISOControl(c)) {
result.append(String.format("\\u%04x", (int) c));
} else {
result.append(c);
}
}
}
result.append('"');
return result.toString();
}
/** Build a string representation of a type and optionally its generic type arguments. */
public static String type(Class<?> raw, String... parameters) {
if (parameters.length == 0) {
return raw.getCanonicalName();
}
if (raw.getTypeParameters().length != parameters.length) {
throw new IllegalArgumentException();
}
StringBuilder result = new StringBuilder();
result.append(raw.getCanonicalName());
result.append("<");
result.append(parameters[0]);
for (int i = 1; i < parameters.length; i++) {
result.append(", ");
result.append(parameters[i]);
}
result.append(">");
return result.toString();
}
@Override public void close() throws IOException {
out.close();
}
/** Emits the modifiers to the writer. */
private void emitModifiers(Set<Modifier> modifiers) throws IOException {
// Use an EnumSet to ensure the proper ordering
if (!(modifiers instanceof EnumSet)) {
modifiers = EnumSet.copyOf(modifiers);
}
for (Modifier modifier : modifiers) {
out.append(modifier.toString()).append(' ');
}
}
private void indent() throws IOException {
for (int i = 0, count = scopes.size(); i < count; i++) {
out.write(indent);
}
}
private void hangingIndent() throws IOException {
for (int i = 0, count = scopes.size() + 2; i < count; i++) {
out.write(indent);
}
}
private void checkInMethod() {
Scope scope = scopes.peekFirst();
if (scope != Scope.NON_ABSTRACT_METHOD && scope != Scope.CONTROL_FLOW
&& scope != Scope.INITIALIZER) {
throw new IllegalArgumentException();
}
}
private void popScope(Scope expected) {
if (scopes.pop() != expected) {
throw new IllegalStateException();
}
}
private enum Scope {
TYPE_DECLARATION,
ABSTRACT_METHOD,
NON_ABSTRACT_METHOD,
CONSTRUCTOR,
CONTROL_FLOW,
ANNOTATION_ATTRIBUTE,
ANNOTATION_ARRAY_VALUE,
INITIALIZER
}
}