package joist.sourcegen;
import static joist.sourcegen.Argument.arg;
import static joist.util.Inflector.capitalize;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import joist.util.Copy;
import joist.util.Function1;
import joist.util.Interpolate;
import joist.util.Join;
import joist.util.StringBuilderr;
public class GClass {
private static final Pattern classNameWithoutGenerics = Pattern.compile("(([a-z][a-zA-Z0-9_]*\\.)*)([A-Z][a-zA-Z0-9_]+)");
public final StringBuilderr staticInitializer = new StringBuilderr();
private final GDirectory directory;
private final ParsedName name;
private final List<GField> fields = new ArrayList<GField>();
private final List<GMethod> methods = new ArrayList<GMethod>();
private final List<GClass> innerClasses = new ArrayList<GClass>();
private final Set<String> imports = new TreeSet<String>();
private final List<GMethod> constructors = new ArrayList<GMethod>();
private final List<String> enumValues = new ArrayList<String>();
private final List<String> implementsInterfaces = new ArrayList<String>();
private final List<String> annotations = new ArrayList<String>();
private Access access = Access.PUBLIC;
private boolean isAbstract = false;
private boolean isInnerClass = false;
private boolean isStaticInnerClass = false;
private boolean isAnonymous = false;
private boolean isEnum = false;
protected boolean isInterface = false;
private String baseClassName = null;
private GClass outerClass;
// this overload is purposefully kept for backwards compatibility
public GClass(String fullClassName) {
this(null, fullClassName);
}
public GClass(GDirectory directory, String fullClassName) {
this.directory = directory;
this.name = ParsedName.parse(fullClassName);
}
public GDirectory getDirectory() {
return this.directory;
}
public GClass setEnum() {
this.isEnum = true;
return this;
}
public GClass setInterface() {
this.isInterface = true;
return this;
}
public GClass addEnumValue(String value, Object... args) {
this.enumValues.add(Interpolate.string(value, args));
return this;
}
public GClass addGetterSetter(String type, String name) {
this.getField(name).type(type);
this.getMethod("get{}", capitalize(name)).returnType(type).body.line("return {};", name);
this.getMethod("set" + capitalize(name), arg(type, name)).body.line("this.{} = {};", name, name);
return this;
}
public boolean isSameClass(String fullNameWithOrWithoutGenerics) {
// ignore generics when considering whether it's the same class
ParsedName otherName = ParsedName.parse(fullNameWithOrWithoutGenerics);
return this.name.getFullName().equals(otherName.getFullName());
}
public GClass getInnerClass(String name, Object... args) {
name = Interpolate.string(name, args);
for (GClass gc : this.innerClasses) {
if (gc.isSameClass(name)) {
return gc;
}
}
GClass gc = new GClass(this.directory, name);
gc.isInnerClass = true;
gc.isStaticInnerClass = true;
gc.outerClass = this;
this.innerClasses.add(gc);
return gc;
}
public GClass notStatic() {
this.isStaticInnerClass = false;
return this;
}
public GMethod getConstructor(String... typeAndNames) {
for (GMethod constructor : this.constructors) {
if (constructor.hasSameArguments(typeAndNames)) {
return constructor;
}
}
GMethod constructor = new GMethod(this, "constructor");
constructor.arguments(typeAndNames).constructorFor(this.getSimpleName());
this.constructors.add(constructor);
return constructor;
}
/** Takes arg0 + _args to differentiate from getConstructor(String... typeAndNames) */
public GMethod getConstructor(Argument arg0, Argument... _args) {
return this.getConstructor(Copy.list(arg0).with(_args));
}
public GMethod getConstructor(List<Argument> _args) {
List<Argument> args = Copy.list(_args).map(new Function1<Argument, Argument>() {
public Argument apply(Argument p1) {
return p1.importIfPossible(GClass.this);
}
});
for (GMethod cstr : this.constructors) {
if (cstr.hasSameArguments(args)) {
return cstr;
}
}
GMethod cstr = new GMethod(this, "constructor").constructorFor(this.getSimpleName());
cstr.arguments(args);
this.constructors.add(cstr);
return cstr;
}
public boolean hasMethod(String name) {
for (GMethod method : this.methods) {
if (method.getName().equals(name)) {
return true;
}
}
return false;
}
// this is weird, but you can call getMethod("methodFoo(String arg1)") and we'll try to do the right thing
public GMethod getMethod(String name, Object... args) {
name = Interpolate.string(name, args);
if (name.indexOf('(') == -1) {
for (GMethod method : this.methods) {
if (method.getName().equals(name)) {
return method;
}
}
GMethod method = new GMethod(this, name);
this.methods.add(method);
return method;
} else {
String typesAndNames = this.stripAndImportPackageIfPossible(name.substring(name.indexOf('(') + 1, name.length() - 1));
name = name.substring(0, name.indexOf('('));
for (GMethod method : this.methods) {
if (method.getName().equals(name) && method.hasSameArguments(typesAndNames)) {
return method;
}
}
GMethod method = new GMethod(this, name);
method.arguments(typesAndNames);
this.methods.add(method);
return method;
}
}
// use arg0 + _args to differentiate between this and getMethod(String, Object...) with only 1 arg
public GMethod getMethod(String name, Argument arg0, Argument... _args) {
return this.getMethod(name, Copy.list(arg0).with(_args));
}
public GMethod getMethod(String name, List<Argument> _args) {
List<Argument> args = Copy.list(_args).map(new Function1<Argument, Argument>() {
public Argument apply(Argument p1) {
return p1.importIfPossible(GClass.this);
}
});
for (GMethod method : this.methods) {
if (method.getName().equals(name) && method.hasSameArguments(args)) {
return method;
}
}
GMethod method = new GMethod(this, name);
method.arguments(args);
this.methods.add(method);
return method;
}
public GField getField(String name, Object... args) {
name = Interpolate.string(name, args);
for (GField field : this.fields) {
if (field.getName().equals(name)) {
return field;
}
}
GField field = new GField(this, name);
this.fields.add(field);
return field;
}
public String toCode() {
StringBuilderr sb = new StringBuilderr();
if (this.name.packageName != null) {
sb.line("package {};", this.name.packageName);
sb.line();
}
if (this.isAnonymous) {
sb.line("new {}() {", this.name.simpleNameWithGenerics);
} else {
if (this.imports.size() > 0) {
for (String importClassName : this.imports) {
sb.line("import {};", importClassName);
}
sb.line();
}
for (String annotation : this.annotations) {
sb.line(annotation);
}
sb.append(this.access.asPrefix());
if (this.isStaticInnerClass) {
sb.append("static ");
}
if (this.isAbstract) {
sb.append("abstract ");
}
if (this.isEnum) {
sb.append("enum ");
} else if (this.isInterface) {
sb.append("interface ");
} else {
sb.append("class ");
}
sb.append(this.name.simpleNameWithGenerics);
sb.append(" ");
if (!this.isInterface) {
if (this.baseClassName != null) {
sb.append("extends {} ", this.stripAndImportPackageIfPossible(this.baseClassName));
}
if (this.implementsInterfaces.size() > 0) {
sb.append("implements {} ", Join.commaSpace(this.implementsInterfaces));
}
} else {
List<String> interfaces = Copy.list();
if (this.baseClassName != null) {
interfaces.add(this.baseClassName);
}
interfaces.addAll(this.implementsInterfaces);
if (!interfaces.isEmpty()) {
sb.append("extends {} ", Join.commaSpace(interfaces));
}
}
sb.line("{");
}
if (this.isEnum && this.enumValues.size() > 0) {
boolean hasMore = this.fields.size() > 0 || this.methods.size() > 0 || this.constructors.size() > 0;
this.lineIfNotAnonymousOrInner(sb);
for (Iterator<String> i = this.enumValues.iterator(); i.hasNext();) {
sb.append(1, i.next());
sb.append(i.hasNext() ? "," : hasMore ? ";" : "");
sb.line();
}
}
if (this.fields.size() > 0) {
this.lineIfNotAnonymousOrInner(sb);
for (GField field : this.fields) {
sb.append(1, field.toCode());
}
}
if (this.staticInitializer.toString().length() > 0) {
sb.line();
sb.line(1, "static {");
sb.append(2, this.staticInitializer.toString());
sb.lineIfNeeded();
sb.line(1, "}");
}
for (GMethod constructor : this.constructors) {
this.lineIfNotAnonymousOrInner(sb);
sb.append(1, constructor.toCode());
}
if (this.methods.size() > 0) {
for (GMethod method : this.methods) {
this.lineIfNotAnonymousOrInner(sb);
sb.append(1, method.toCode());
}
}
if (this.innerClasses.size() > 0) {
for (GClass gc : this.innerClasses) {
sb.line();
sb.append(1, gc.toCode());
}
}
this.lineIfNotAnonymousOrInner(sb);
sb.line("}");
return sb.toString();
}
public GClass setAbstract() {
this.isAbstract = true;
return this;
}
public GClass setPackagePrivate() {
this.access = Access.PACKAGE;
return this;
}
public GClass setPrivate() {
this.access = Access.PRIVATE;
return this;
}
public GClass setAccess(Access access) {
this.access = access;
return this;
}
public GClass setAnonymous() {
this.isAnonymous = true;
return this;
}
public GClass addImports(Class<?>... types) {
if (this.outerClass != null) {
this.outerClass.addImports(types);
return this;
}
for (Class<?> type : types) {
this.addImports(type.getName());
}
return this;
}
public GClass addImports(String... importClassNames) {
if (this.outerClass != null) {
this.outerClass.addImports(importClassNames);
return this;
}
for (String importClassName : importClassNames) {
ParsedName name = ParsedName.parse(importClassName);
String packageName = name.packageName;
if (packageName == null || packageName.equals(this.name.packageName) || "java.lang".equals(packageName)) {
continue;
}
this.imports.add(name.packageName + "." + name.simpleName);
}
return this;
}
public String getBaseClassName() {
return this.baseClassName;
}
public String getBaseQualifiedClassName() {
String fqcn = this.baseClassName;
if (fqcn != null && !fqcn.contains(".")) {
fqcn = this.getPackageName() + "." + fqcn;
}
return fqcn;
}
public GClass baseClass(Class<?> type) {
return this.baseClassName(type.getName());
}
public GClass baseClassName(String baseClassName, Object... args) {
this.baseClassName = this.stripAndImportPackageIfPossible(Interpolate.string(baseClassName, args));
return this;
}
/** @return the short name if this was importable or else the full name if a name collision would have happened */
public String stripAndImportPackageIfPossible(String fullClassName) {
Matcher m = GClass.classNameWithoutGenerics.matcher(fullClassName);
while (m.find()) {
String packageName = m.group(1).replaceAll("\\.$", "");
String simpleName = m.group(3);
if (!"".equals(packageName) && !this.isImportAlreadyTakenByDifferentPackage(packageName, simpleName)) {
fullClassName = fullClassName.replaceFirst(packageName + "\\." + simpleName, simpleName);
this.addImports(packageName + "." + simpleName);
}
}
return fullClassName;
}
private boolean isImportAlreadyTakenByDifferentPackage(String packageName, String simpleName) {
for (String existingImport : this.imports) {
if (!existingImport.equals(packageName + "." + simpleName) && existingImport.endsWith("." + simpleName)) {
return true;
}
}
if (simpleName.equals(this.name.simpleName) && !packageName.equals(this.name.packageName)) {
return true;
}
return false;
}
private void lineIfNotAnonymousOrInner(StringBuilderr sb) {
if (!this.isAnonymous && !this.isInnerClass) {
sb.line();
}
}
public List<GMethod> getConstructors() {
return this.constructors;
}
public String toString() {
return this.getFullName();
}
/** @return the package name */
public String getPackageName() {
return this.name.packageName;
}
/** @return the simple name without generics */
public String getSimpleName() {
return this.name.simpleName;
}
/** @return the package + simple name without generics */
public String getFullName() {
return this.name.getFullName();
}
/** @return the relative file name, e.g. {@code com/foo/Bar.java}. */
public String getFileName() {
return this.getFullName().replace(".", File.separator) + ".java";
}
public GClass implementsInterface(Class<?> interfaceClass) {
this.implementsInterfaces.add(this.stripAndImportPackageIfPossible(interfaceClass.getName()));
return this;
}
public GClass implementsInterface(String interfaceFullName, Object... args) {
interfaceFullName = Interpolate.string(interfaceFullName, args);
this.implementsInterfaces.add(this.stripAndImportPackageIfPossible(interfaceFullName));
return this;
}
public GClass addAnnotation(String annotation, Object... args) {
this.annotations.add(Interpolate.string(annotation, args));
return this;
}
public GClass addEquals() {
return this.addEquals(this.getFieldNames());
}
public GClass addEquals(String... fieldNames) {
return this.addEquals(Arrays.asList(fieldNames));
}
public GClass addEquals(Collection<String> fieldNames) {
GMethod equals = this.getMethod("equals", arg("Object", "other")).returnType("boolean").addAnnotation("@Override");
if (this.name.hasGenerics()) {
equals.addAnnotation("@SuppressWarnings(\"unchecked\")");
}
equals.body.line("if (other != null && other instanceof {}) {", this.name.simpleName);
if (this.fields.size() == 0) {
equals.body.line("_ return true;");
} else {
equals.body.line("_ final {} o = ({}) other;", this.name.simpleNameWithGenerics, this.name.simpleNameWithGenerics);
equals.body.line("_ return true"); // leave open
for (String fieldName : fieldNames) {
GField field = this.findField(fieldName);
if (Primitives.isPrimitive(field.getTypeClassName())) {
equals.body.line("_ _ && o.{} == this.{}", field.getName(), field.getName());
} else if (field.getTypeClassName().endsWith("[]")) {
equals.body.line("_ _ && java.util.Arrays.deepEquals(o.{}, this.{})", field.getName(), field.getName());
} else {
equals.body.line(
"_ _ && ((o.{} == null && this.{} == null) || (o.{} != null && o.{}.equals(this.{})))",
field.getName(),
field.getName(),
field.getName(),
field.getName(),
field.getName());
}
}
equals.body.line("_ ;"); // finally close
}
equals.body.line("}");
equals.body.line("return false;");
return this;
}
public GClass addHashCode() {
return this.addHashCode(this.getFieldNames());
}
public GClass addHashCode(String... fieldNames) {
return this.addHashCode(Arrays.asList(fieldNames));
}
public GClass addHashCode(Collection<String> fieldNames) {
GMethod hashCode = this.getMethod("hashCode").returnType("int").addAnnotation("@Override");
hashCode.body.line("int hashCode = 23;");
hashCode.body.line("hashCode = (hashCode * 37) + getClass().hashCode();");
for (String fieldName : fieldNames) {
GField field = this.findField(fieldName);
String prefix = "hashCode = (hashCode * 37) + ";
if (Primitives.isPrimitive(field.getTypeClassName())) {
hashCode.body.line(prefix + "new {}({}).hashCode();", Primitives.getWrapper(field.getTypeClassName()), field.getName());
} else if (field.getTypeClassName().endsWith("[]")) {
hashCode.body.line(prefix + "java.util.Arrays.deepHashCode({});", field.getName());
} else {
hashCode.body.line(prefix + "({} == null ? 1 : {}.hashCode());", field.getName(), field.getName());
}
}
hashCode.body.line("return hashCode;");
return this;
}
public GClass addToString() {
return this.addToString(this.getFieldNames());
}
public GClass addToString(String... fieldNames) {
return this.addToString(Arrays.asList(fieldNames));
}
public GClass addToString(Collection<String> fieldNames) {
GMethod tos = this.getMethod("toString").returnType("String").addAnnotation("@Override");
tos.body.line("return \"{}[\"", this.getSimpleName());
int i = 0;
for (String fieldName : fieldNames) {
GField field = this.findField(fieldName);
if (field.getTypeClassName().endsWith("[]")) {
tos.body.line("_ + java.util.Arrays.toString({})", field.getName());
} else {
tos.body.line("_ + {}", field.getName());
}
if (++i < fieldNames.size()) {
tos.body.line("_ + \", \"");
}
}
tos.body.line("_ + \"]\";");
return this;
}
private GField findField(String name) {
for (GField field : this.fields) {
if (field.getName().equals(name)) {
return field;
}
}
if (this.directory != null) {
String currentBaseName = this.getBaseQualifiedClassName();
while (currentBaseName != null) {
GClass currentBase = this.directory.getClass(currentBaseName);
for (GField field : currentBase.fields) {
if (field.getName().equals(name)) {
return field;
}
}
currentBaseName = currentBase.getBaseQualifiedClassName();
}
}
throw new IllegalArgumentException("Could not find field " + name);
}
private List<String> getFieldNames() {
List<String> fieldNames = new ArrayList<String>();
for (GField field : this.fields) {
fieldNames.add(field.getName());
}
return fieldNames;
}
}