blob: d93d259bc9bf4dd088260d7186714dbd52b9c0cc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.util;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import org.apache.openjpa.lib.util.Localizer;
/**
* A utility to help writing Java Source code dynamically.
*
* Provides basic elements of Java Source Code e.g. Package, Class, Field,
* Method, Import, Annotation, Argument.
*
* Mutator methods return the operating element for easy chaining.
*
* @author Pinaki Poddar
*
* @since 2.0.0
*
*/
public class SourceCode {
private static Localizer _loc = Localizer.forPackage(SourceCode.class);
/**
* List of Java Keywords and primitive types. Populated statically.
*/
private static final ArrayList<String> reserved = new ArrayList<>();
private static final ArrayList<String> knownTypes = new ArrayList<>();
private static int TABSIZE = 4;
private static final String SPACE = " ";
private static final String BLANK = "";
private static final String SEMICOLON = ";";
public static final String COMMA = ",";
public static final String DOT = ".";
public static final String EQUAL = "=";
public static final String QUOTE = "\"";
private static final Delimiter BLOCK_DELIMITER = new Delimiter("{}");
private static final Delimiter ARGS_DELIMITER = new Delimiter("()");
private static final Delimiter PARAMS_DELIMITER = new Delimiter("<>");
private List<Comment> comments;
private final Package pkg;
private final Class cls;
private final Set<Import> imports = new TreeSet<>();
/**
* Create source code for a top-level class with given fully-qualified
* class name.
*/
public SourceCode(String c) {
ClassName name = getOrCreateImport(c);
this.cls = new Class(c);
this.pkg = new Package(name.getPackageName());
}
/**
* Gets the top level class represented by this receiver.
*/
public Class getTopLevelClass() {
return cls;
}
public Package getPackage() {
return pkg;
}
/**
* Sets the tab size. Tabs are always written as spaces.
*/
public SourceCode setTabSize(int t) {
if (t>0) TABSIZE = Math.max(t, 8);
return this;
}
/**
* Adds import to this source code. Adding an import may force the given class name
* to use its full name if it is hidden by other imports.
*
* @param name a ClassName instance
* @return true if the import is added. ClassName starting with <code>java.lang.</code>
* is not added.
*/
private boolean addImport(ClassName name) {
String pkgName = name.getPackageName();
for (Import i : imports) {
if (i.getClassName().hides(name)) {
i.getClassName().useFullName();
name.useFullName();
}
}
return imports.add(new Import(name));
}
/**
* Get the class name instance for the given fully-qualified class name.
* If the given class name is already imported, then use the existing instance.
* Otherwise, creates a new instance and adds it to list of imports.
*
* @see #addImport(ClassName)
* @see ClassName
*
* @param name fully-qualified name of a class
* @return an existing class name instance or a new one.
*/
public ClassName getOrCreateImport(String name) {
for (Import i : imports) {
if (i.name.getFullName().equals(name))
return i.name;
}
ClassName imp = new ClassName(name);
addImport(imp);
return imp;
}
public SourceCode addComment(boolean inline, String... lines) {
if (lines == null)
return this;
if (comments == null)
comments = new ArrayList<>();
Comment comment = new Comment();
comments.add(comment);
comment.makeInline(inline);
for (String line:lines) {
// Handle long header comment lines...
if (line.length() > 120-4) {
String[] wrappedLines = wrap(line, 120-4);
for (String w:wrappedLines) {
comment.append(w);
}
} else {
comment.append(line);
}
}
return this;
}
/**
* Prints the class to the given Writer.
* @param out
*/
public void write(PrintWriter out) {
if (comments != null) {
for (Comment comment : comments) {
comment.write(out, 0);
out.println();
}
}
if (pkg != null) {
pkg.write(out,0);
out.println();
}
for (Import imp:imports) {
imp.write(out, 0);
}
out.println();
cls.write(out, 0);
out.flush();
}
/**
* Outputs <code>tab</code> number of spaces.
*/
static void tab(PrintWriter out, int tab) {
for (int i=0; i<tab*TABSIZE; i++) {
out.print(SPACE);
}
}
/**
* Wraps the given string into lines of max length width at word boundaries
*/
public static String[] wrap(String longLine, int width) {
String[] words = longLine.split("\\ ");
List<String> lines = new ArrayList<>();
StringBuilder line = new StringBuilder();
for (String w : words) {
if (line.length() + w.length() < width) {
if (line.length() > 0) line.append(" ");
line.append(w);
}
else {
lines.add(line.toString());
line.setLength(0);
line.append(w);
}
}
lines.add(line.toString());
return lines.toArray(new String[lines.size()]);
}
static void writeList(PrintWriter out, String header, List<?> list) {
writeList(out, header, list, new Delimiter(), false);
}
static void writeList(PrintWriter out, String header, List<?> list,
Delimiter bracket, boolean writeEmpty) {
if (list == null || list.isEmpty()) {
if (writeEmpty)
out.append(bracket.start)
.append(bracket.end);
return;
}
out.append(header);
out.append(bracket.start);
for (int i=0; i<list.size(); i++) {
out.append(list.get(i).toString());
if (i!=list.size()-1) out.append(COMMA);
}
out.append(bracket.end);
}
static String capitalize(String s) {
return Character.toUpperCase(s.charAt(0))+s.substring(1);
}
static boolean isValidToken(String s) {
return s != null && s.length() > 0 &&
!reserved.contains(s) && isJavaIdentifier(s);
}
public static boolean isKnownType(String s) {
return knownTypes.contains(s);
}
static boolean isEmpty(String s) {
return s == null || s.length()==0;
}
static LinkedList<String> tokenize(String s, String delim) {
StringTokenizer tokenizer = new StringTokenizer(s, delim, false);
LinkedList<String> tokens = new LinkedList<>();
while (tokenizer.hasMoreTokens())
tokens.add(tokenizer.nextToken());
return tokens;
}
public static boolean isJavaIdentifier(String s) {
if (s == null || s.length() == 0 ||
!Character.isJavaIdentifierStart(s.charAt(0))) {
return false;
}
for (int i=1; i<s.length(); i++) {
if (!Character.isJavaIdentifierPart(s.charAt(i))) {
return false;
}
}
return true;
}
public enum ACCESS {PUBLIC, PROTECTED, PRIVATE}
/**
* Abstract element has a name, optional list of modifiers, annotations
* and arguments.
*/
public abstract class Element<T> implements Comparable<Element<T>> {
protected String name;
protected ClassName type;
protected ACCESS access;
protected boolean isStatic;
protected boolean isFinal;
protected Comment comment;
protected List<ClassName> params = new ArrayList<>();
protected List<Annotation> annos = new ArrayList<>();
protected Element(String name, ClassName type) {
this.name = name;
this.type = type;
}
public ClassName getType() {
return type;
}
public Annotation addAnnotation(String a) {
Annotation an = new Annotation(a);
annos.add(an);
return an;
}
public Element<T> addParameter(String param) {
params.add(getOrCreateImport(param));
return this;
}
@Override
public int compareTo(Element<T> other) {
return name.compareTo(other.name);
}
public T addComment(boolean inline, String... lines) {
if (comment == null) {
comment = new Comment();
}
comment.makeInline(inline);
for (String line:lines) {
comment.append(line);
}
return (T)this;
}
public T makePublic() {
access = ACCESS.PUBLIC;
return (T)this;
}
public T makeProtected() {
access = ACCESS.PROTECTED;
return (T)this;
}
public T makePrivate() {
access = ACCESS.PRIVATE;
return (T)this;
}
public T makeStatic() {
isStatic = true;
return (T)this;
}
public T makeFinal() {
isFinal = true;
return (T)this;
}
public void write(PrintWriter out, int tab) {
if (comment != null) comment.write(out, tab);
for (Annotation a:annos)
a.write(out, tab);
tab(out, tab);
if (access != null)
out.append(access.toString().toLowerCase(Locale.ENGLISH) + SPACE);
if (isStatic)
out.append("static" + SPACE);
if (isFinal)
out.append("final" + SPACE);
}
}
/**
* Represent <code>class</code> declaration.
*
*/
public class Class extends Element<Class> {
private boolean isAbstract;
private boolean isFinal;
private ClassName superCls;
private List<ClassName> interfaces = new ArrayList<>();
private Set<Field> fields = new TreeSet<>();
private Set<Method> methods = new TreeSet<>();
private Set<Constructor> constructors = new TreeSet<>();
public Class(String name) {
super(name, getOrCreateImport(name));
makePublic();
}
public Class setSuper(String s) {
superCls = getOrCreateImport(s);
return this;
}
public Class addInterface(String s) {
interfaces.add(getOrCreateImport(s));
return this;
}
public Class makeAbstract() {
if (isFinal)
throw new IllegalArgumentException(_loc.get("src-invalid-modifier").toString());
isAbstract = true;
return this;
}
@Override
public Class makeFinal() {
if (isAbstract)
throw new IllegalArgumentException(_loc.get("src-invalid-modifier").toString());
isFinal = true;
return this;
}
/**
* Adds getters and setters to every non-public field.
*/
public Class markAsBean() {
for (Field f:fields)
f.markAsBean();
return this;
}
public String getName() {
return getType().getSimpleName();
}
public String getPackageName() {
return getType().getPackageName();
}
public Field addField(String name, String type) {
return addField(name, getOrCreateImport(type));
}
public Field addField(String f, ClassName type) {
if (!isValidToken(f)) {
throw new IllegalArgumentException(
_loc.get("src-invalid-field",f).toString());
}
Field field = new Field(this, f, type);
if (!fields.add(field))
throw new IllegalArgumentException(_loc.get(
"src-duplicate-field", field, this).toString());
return field;
}
public Method addMethod(String m, String retType) {
return addMethod(m, getOrCreateImport(retType));
}
protected Method addMethod(String m, ClassName retType) {
if (isEmpty(m) || !isValidToken(m)) {
throw new IllegalArgumentException(_loc.get(
"src-invalid-method",m).toString());
}
Method method = new Method(m, retType);
if (!methods.add(method))
throw new IllegalArgumentException(_loc.get(
"src-duplicate-method", method, this).toString());
return method;
}
public Constructor addConstructor(){
Constructor c = new Constructor(type.simpleName);
if (!constructors.add(c))
throw new IllegalArgumentException(_loc.get(
"src-duplicate-constructor", c, this).toString());
return c;
}
@Override
public void write(PrintWriter out, int tab) {
super.write(out, tab);
if (isAbstract)
out.append("abstract ");
if(isFinal)
out.append("final ");
out.print("class ");
out.print(type.simpleName);
writeList(out, BLANK, params, PARAMS_DELIMITER, false);
if (superCls != null)
out.print(" extends " + superCls + SPACE);
writeList(out, "implements ", interfaces);
out.println(SPACE + BLOCK_DELIMITER.start);
for (Field field:fields)
field.write(out, 1);
for(Constructor ctor : constructors){
ctor.write(out, 1);
}
for (Method method:methods)
method.write(out, 1);
out.println(BLOCK_DELIMITER.end);
}
@Override
public String toString() {
return getType().fullName;
}
}
/**
* Represents field declaration.
*
*/
public class Field extends Element<Field> {
private final Class owner;
protected boolean isTransient;
protected boolean isVolatile;
Field(Class owner, String name, ClassName type) {
super(name, type);
this.owner = owner;
makePrivate();
}
/**
* Adds bean-style getter setter method.
*/
public Field markAsBean() {
addGetter();
addSetter();
return this;
}
public Field addGetter() {
owner.addMethod("get"+ capitalize(name), type)
.makePublic()
.addCodeLine("return "+ name);
return this;
}
public Field addSetter() {
owner.addMethod("set"+ capitalize(name), "void")
.makePublic()
.addArgument(new Argument<>(type, name,SPACE))
.addCodeLine("this."+ name + " = " + name);
return this;
}
public void makeVolatile() {
isVolatile = true;
}
public void makeTransient() {
isTransient = true;
}
@Override
public String toString() {
return type + SPACE + name;
}
@Override
public void write(PrintWriter out, int tab) {
super.write(out, tab);
if (isVolatile) out.print("volatile ");
if (isTransient) out.print("transient ");
out.print(type);
writeList(out, BLANK, params, PARAMS_DELIMITER, false);
out.println(SPACE + name + SEMICOLON);
}
@Override
public boolean equals(Object other) {
if (other instanceof Field) {
Field that = (Field)other;
return name.equals(that.name);
}
return false;
}
}
/**
* Represents Method declaration.
*
*
*/
public class Method extends Element<Method> {
private boolean isAbstract;
private List<Argument<ClassName,String>> args = new ArrayList<>();
private List<String> codeLines = new ArrayList<>();
int tabCount = 0;
String tab = "";
Method(String n, String t) {
this(n, getOrCreateImport(t));
}
public Method(String name, ClassName returnType) {
super(name, returnType);
makePublic();
}
public Method addArgument(Argument<ClassName,String> arg) {
args.add(arg);
return this;
}
public Method addArgument(String className, String argName){
ClassName cn = getOrCreateImport(className);
args.add(new Argument<>(cn, argName," "));
return this;
}
public void setTab(boolean inc) {
if (inc)
tabCount++;
else
tabCount--;
tab = "";
for (int i = 0; i < tabCount * TABSIZE; i++) {
tab += SPACE;
}
}
public Method addCodeLine(String line) {
if (isAbstract)
throw new IllegalStateException("abstract method " + name + " can not have body");
// This doesn't handle try{ ... catch(){ if{
if (line.endsWith("{") || line.endsWith("}")) {
}
if (!line.endsWith(SEMICOLON)
&& !(line.isEmpty() || line.endsWith("{") || line.endsWith("}") || line.startsWith("if")))
line = line + SEMICOLON;
codeLines.add(tab + line);
return this;
}
/**
* if tabInc = true, the current line, and all following lines will be tabbed.
* If false, a tab will be removed.
*/
public Method addCodeLine(String line, boolean tabInc) {
setTab(tabInc);
return addCodeLine(line);
}
public Method makeAbstract() {
if (codeLines.isEmpty())
isAbstract = true;
else
throw new IllegalStateException("method " + name +
" can not be abstract. It has a body");
return this;
}
@Override
public String toString() {
return type + SPACE + name;
}
@Override
public void write(PrintWriter out, int tab) {
out.println(BLANK);
super.write(out, tab);
if (isAbstract) out.append("abstract ");
out.print(type + SPACE + name);
writeList(out, BLANK, args, ARGS_DELIMITER, true);
if (isAbstract) {
out.println(SEMICOLON);
return;
}
out.println(SPACE + BLOCK_DELIMITER.start);
for (String line : codeLines) {
tab(out, tab+1);
out.println(line);
}
tab(out, tab);
out.println(BLOCK_DELIMITER.end);
}
@Override
public boolean equals(Object other) {
if (other instanceof Method) {
Method that = (Method)other;
return name.equals(that.name) && args.equals(that.args);
}
return false;
}
}
public class Constructor extends Element<Constructor> {
private List<Argument<ClassName,String>> args = new ArrayList<>();
private List<String> codeLines = new ArrayList<>();
int tabCount = 0;
String tab = "";
public Constructor(String name) {
super(name, null);
makePublic();
}
public Constructor addArgument(Argument<ClassName,String> arg) {
args.add(arg);
return this;
}
public Constructor addArgument(String className, String argName) {
ClassName cn = getOrCreateImport(className);
args.add(new Argument<>(cn, argName, " "));
return this;
}
public Constructor addCodeLine(String line) {
// This doesn't handle try{ ... catch(){ if{
if (line.endsWith("{") || line.endsWith("}")) {
}
if (!line.endsWith(SEMICOLON)
&& !(line.isEmpty() || line.endsWith("{") || line.endsWith("}") || line.startsWith("if")))
line = line + SEMICOLON;
codeLines.add(tab + line);
return this;
}
/**
* if tabInc = true, the current line, and all following lines will be tabbed. If false, a tab will be removed.
*/
public Constructor addCodeLine(String line, boolean tabInc) {
setTab(tabInc);
return addCodeLine(line);
}
public void setTab(boolean inc) {
if (inc)
tabCount++;
else
tabCount--;
tab = "";
for (int i = 0; i < tabCount * TABSIZE; i++) {
tab += SPACE;
}
}
@Override
public void write(PrintWriter out, int tab) {
out.println(BLANK);
super.write(out, tab);
out.print(name);
writeList(out, BLANK, args, ARGS_DELIMITER, true);
out.println(SPACE + BLOCK_DELIMITER.start);
for (String line : codeLines) {
tab(out, tab+1);
out.println(line);
}
tab(out, tab);
out.println(BLOCK_DELIMITER.end);
}
}
/**
* Represents <code>import</code> statement.
*
*/
class Import implements Comparable<Import> {
private final ClassName name;
public Import(ClassName name) {
this.name = name;
}
@Override
public int compareTo(Import other) {
return name.compareTo(other.name);
}
public void write(PrintWriter out, int tab) {
if (name.usingFullName())
return;
String pkg = name.getPackageName();
if (pkg.length() == 0 || pkg.equals(getPackage().name))
return;
out.println("import "+ name.fullName + SEMICOLON);
}
@Override
public boolean equals(Object other) {
if (other instanceof Import) {
Import that = (Import)other;
return name.equals(that.name);
}
return false;
}
ClassName getClassName() {
return name;
}
}
/**
* Represents method argument.
*
*/
public class Argument<K,V> {
final private K key;
final private V value;
final private String connector;
Argument(K key, V value, String connector) {
this.key = key;
this.value = value;
this.connector = connector;
}
@Override
public String toString() {
return key + connector + value;
}
}
/**
* Represents annotation.
*
*/
public class Annotation {
private String name;
private List<Argument<?,?>> args = new ArrayList<>();
Annotation(String n) {
name = n;
}
public Annotation addArgument(String key, String v, boolean quote) {
return addArgument(new Argument<>(key,
quote ? quote(v) : v, EQUAL));
}
public Annotation addArgument(String key, String v) {
return addArgument(key, v, true);
}
public Annotation addArgument(String key, String[] vs) {
StringBuilder tmp = new StringBuilder(BLOCK_DELIMITER.start);
for (int i=0; i < vs.length; i++) {
tmp.append(quote(vs[i]));
tmp.append(i != vs.length-1 ? COMMA : BLANK);
}
tmp.append(BLOCK_DELIMITER.end);
return addArgument(key, tmp.toString(), false);
}
public <K,V> Annotation addArgument(Argument<K,V> arg) {
args.add(arg);
return this;
}
public void write(PrintWriter out, int tab) {
tab(out, tab);
out.println("@"+name);
writeList(out, BLANK, args, ARGS_DELIMITER, false);
out.println();
}
String quote(String s) {
return QUOTE + s + QUOTE;
}
}
static class Package {
private String name;
Package(String p) {
name = p;
}
public void write(PrintWriter out, int tab) {
if (name != null && !name.isEmpty())
out.println("package " + name + SEMICOLON);
}
}
class Comment {
List<String> lines = new ArrayList<>();
private boolean inline = false;
public void append(String line) {
lines.add(line);
}
boolean isEmpty() {
return lines.isEmpty();
}
void makeInline(boolean flag) {
inline = flag;
}
public void write(PrintWriter out, int tab) {
if (inline) {
for (String l:lines) {
tab(out, tab);
out.println("// " + l);
}
} else {
int i = 0;
for (String l:lines) {
tab(out, tab);
if (i == 0) {
out.println("/** ");
tab(out, tab);
}
out.println(" * " + l);
i++;
}
tab(out, tab);
out.println("**/");
}
}
}
/**
* Represents fully-qualified name of a Java type.
*
* NOTE: Do not construct directly unless necessary.
* @see SourceCode#getOrCreateImport(String)
*/
private class ClassName implements Comparable<ClassName> {
public final String fullName;
public final String simpleName;
public final String pkgName;
private String arrayMarker = BLANK;
private boolean useFullName = false;
ClassName(String name) {
while (isArray(name)) {
arrayMarker = arrayMarker + "[]";
name = getComponentName(name);
}
int start = name.indexOf("<");
int stop = name.lastIndexOf(">");
if (start != -1 && stop != -1) {
name = name.substring(0, start) + name.substring(stop + 1);
}
this.fullName = name;
int dot = fullName.lastIndexOf(DOT);
simpleName = (dot == -1) ? fullName : fullName.substring(dot+1);
pkgName = (dot == -1) ? BLANK : fullName.substring(0,dot);
if (!isValidTypeName(name)) {
throw new IllegalArgumentException(_loc.get("src-invalid-type",
name).toString());
}
}
/**
* Gets fully qualified name of this receiver.
*/
public String getFullName() {
return fullName + arrayMarker;
}
/**
* Gets simple name of this receiver.
*/
public String getSimpleName() {
return simpleName + arrayMarker;
}
/**
* Gets the package name of this receiver. Default package name is
* represented as empty string.
*/
public String getPackageName() {
return pkgName;
}
/**
* Gets the full or simple name of this receiver based on useFullName flag.
*/
@Override
public String toString() {
return (useFullName ? fullName : simpleName) + arrayMarker;
}
/**
* Compares by fully-qualified name.
*/
@Override
public int compareTo(ClassName other) {
return getFullName().compareTo(other.getFullName());
}
public boolean isValidTypeName(String s) {
return isValidPackageName(pkgName)
&& (isKnownType(s) || isValidToken(simpleName));
}
boolean isValidPackageName(String s) {
if (isEmpty(s)) return true;
LinkedList<String> tokens = tokenize(s, DOT);
for (String token : tokens) {
if (!isValidToken(token))
return false;
}
return !s.endsWith(DOT);
}
boolean isArray(String name) {
return name.endsWith("[]");
}
String getComponentName(String name) {
return (!isArray(name)) ? name :
name.substring(0, name.length()-"[]".length());
}
boolean hides(ClassName other) {
return this.getSimpleName().equals(other.getSimpleName())
&& !this.fullName.equals(other.fullName);
}
void useFullName() {
useFullName = true;
}
boolean usingFullName() {
return useFullName;
}
}
static class Delimiter {
final char start;
final char end;
public Delimiter() {
this((char)' ', (char)' ');
}
public Delimiter(String pair) {
this(pair.charAt(0), pair.charAt(1));
}
public Delimiter(char start, char end) {
super();
this.start = start;
this.end = end;
}
}
static {
reserved.add("abstract");
reserved.add("continue");
reserved.add("for");
reserved.add("new");
reserved.add("switch");
reserved.add("assert");
reserved.add("default");
reserved.add("goto");
reserved.add("package");
reserved.add("synchronized");
reserved.add("boolean");
reserved.add("do");
reserved.add("if");
reserved.add("private");
reserved.add("this");
reserved.add("break");
reserved.add("double");
reserved.add("implements");
reserved.add("protected");
reserved.add("throw");
reserved.add("byte");
reserved.add("else");
reserved.add("import");
reserved.add("public");
reserved.add("throws");
reserved.add("case");
reserved.add("enum");
reserved.add("instanceof");
reserved.add("return");
reserved.add("transient");
reserved.add("catch");
reserved.add("extends");
reserved.add("int");
reserved.add("short");
reserved.add("try");
reserved.add("char");
reserved.add("final");
reserved.add("interface");
reserved.add("static");
reserved.add("void");
reserved.add("class");
reserved.add("finally");
reserved.add("long");
reserved.add("strictfp");
reserved.add("volatile");
reserved.add("const");
reserved.add("float");
reserved.add("native");
reserved.add("super");
reserved.add("while");
knownTypes.add("boolean");
knownTypes.add("byte");
knownTypes.add("char");
knownTypes.add("double");
knownTypes.add("float");
knownTypes.add("int");
knownTypes.add("long");
knownTypes.add("short");
knownTypes.add("void");
knownTypes.add("String");
}
}