/* * 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.*; 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<String>(); private static final ArrayList<String> knownTypes = new ArrayList<String>(); 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<Import>(); /** * 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 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<String>(); StringBuilder line = new StringBuilder(); for (int i = 0; i < words.length; i++) { String w = words[i]; 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<String>(); 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<ClassName>(); protected List<Annotation> annos = new ArrayList<Annotation>(); 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; } 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<ClassName>(); private Set<Field> fields = new TreeSet<Field>(); private Set<Method> methods = new TreeSet<Method>(); private Set<Constructor> constructors = new TreeSet<Constructor>(); 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; } 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; } 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); } 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<ClassName,String>(type, name,SPACE)) .addCodeLine("this."+ name + " = " + name); return this; } public void makeVolatile() { isVolatile = true; } public void makeTransient() { isTransient = true; } public String toString() { return type + SPACE + name; } 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); } 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<Argument<ClassName,String>>(); private List<String> codeLines = new ArrayList<String>(); 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<ClassName, String>(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; } public String toString() { return type + SPACE + name; } 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); } 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<Argument<ClassName,String>>(); private List<String> codeLines = new ArrayList<String>(); 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<ClassName, String>(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; } 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); } 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; } public String toString() { return key + connector + value; } } /** * Represents annotation. * */ public class Annotation { private String name; private List<Argument<?,?>> args = new ArrayList<Argument<?,?>>(); Annotation(String n) { name = n; } public Annotation addArgument(String key, String v, boolean quote) { return addArgument(new Argument<String,String>(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<String>(); 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. */ public String toString() { return (useFullName ? fullName : simpleName) + arrayMarker; } /** * Compares by fully-qualified name. */ 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"); } }