/* * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package builder; import toolbox.ToolBox; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Builder for type declarations. * Note: this implementation does not support everything and is not * exhaustive. */ public class ClassBuilder extends AbstractBuilder { private final ToolBox tb; private final String fqn; private final String clsname; private final String typeParameter; private String pkg; private final List imports; private String extendsType; private final List implementsTypes; private final List members; private final List inners; private final List nested; final static Pattern CLASS_RE = Pattern.compile("(.*)(<.*>)"); /** * Creates a class builder. * @param tb the toolbox reference * @param name the name of the type */ public ClassBuilder(ToolBox tb, String name) { super(new Modifiers(), name); this.tb = tb; Matcher m = CLASS_RE.matcher(name); if (m.matches()) { fqn = m.group(1); typeParameter = m.group(2); } else { fqn = name; typeParameter = null; } if (fqn.contains(".")) { this.pkg = name.substring(0, fqn.lastIndexOf('.')); clsname = fqn.substring(fqn.lastIndexOf('.') + 1); } else { clsname = fqn; } imports = new ArrayList<>(); implementsTypes = new ArrayList<>(); members = new ArrayList<>(); nested = new ArrayList<>(); inners = new ArrayList<>(); } /** * Adds an import(s). * @param i the import type. * @return this builder. */ public ClassBuilder addImports(String i) { imports.add(i); return this; } /** * Sets the modifiers for this builder. * @param modifiers the modifiers * @return this builder */ public ClassBuilder setModifiers(String... modifiers) { this.modifiers.setModifiers(modifiers); return this; } /** * Sets the enclosing type's name. * * @param className the enclosing type's name */ @Override void setClassName(String className) { cname = className; } /** * Sets a comment for the element. * * @param comments for the element * @return this builder. */ @Override public ClassBuilder setComments(String... comments) { super.setComments(comments); return this; } /** * Sets a comment for the element. Typically used to set * the user's preferences whether an automatic comment is * required or no API comment. * * @param kind of comment, automatic or no comment. * @return this builder. */ @Override public ClassBuilder setComments(Comment.Kind kind) { super.setComments(kind); return this; } /** * Set the super-type of the type. * @param name of the super type. * @return this builder. */ public ClassBuilder setExtends(String name) { extendsType = name; return this; } /** * Adds an implements declaration(s). * @param names the interfaces * @return this builder. */ public ClassBuilder addImplements(String... names) { implementsTypes.addAll(List.of(names)); return this; } /** * Adds a member(s) to the class declaration. * @param mbs the member builder(s) representing member(s). * @return this builder */ public ClassBuilder addMembers(MemberBuilder... mbs) { for (MemberBuilder mb : mbs) { members.add(mb); mb.setClassName(fqn); } return this; } /** * Adds nested-classes, to an outer class to an outer builder. * @param cbs class builder(s) of the nested classes. * @return this builder. */ public ClassBuilder addNestedClasses(ClassBuilder... cbs) { Stream.of(cbs).forEach(cb -> { nested.add(cb); cb.setClassName(fqn); }); return this; } /** * Adds inner-classes, to an outer class builder. * @param cbs class builder(s) of the inner classes. * @return this builder. */ public ClassBuilder addInnerClasses(ClassBuilder... cbs) { Stream.of(cbs).forEach(cb -> { inners.add(cb); cb.setClassName(fqn); }); return this; } @Override public String toString() { OutputWriter ow = new OutputWriter(); if (pkg != null) ow.println("package " + pkg + ";"); imports.forEach(i -> ow.println("import " + i + ";")); switch (comments.kind) { case AUTO: ow.println("/** Class " + fqn + " */"); break; case USER: ow.println("/** "); comments.comments.forEach(c -> ow.println(" * " + c)); ow.println(" */"); break; case NO_API_COMMENT: ow.println("// NO_API_COMMENT"); break; } ow.print(modifiers.toString()); ow.print(clsname); if (typeParameter != null) { ow.print(typeParameter + " "); } else { ow.print(" "); } if (extendsType != null && !extendsType.isEmpty()) { ow.print("extends " + extendsType + " "); } if (!implementsTypes.isEmpty()) { ow.print("implements "); ListIterator iter = implementsTypes.listIterator(); while (iter.hasNext()) { String s = iter.next() ; ow.print(s); if (iter.hasNext()) ow.print(", "); } } ow.print("{"); if (!nested.isEmpty()) { ow.println(""); nested.forEach(m -> ow.println(m.toString())); } if (!members.isEmpty()) { ow.println(""); members.forEach(m -> ow.println(m.toString())); } ow.println("}"); if (!inners.isEmpty()) { ow.println(" {"); inners.forEach(m -> ow.println(m.toString())); ow.println("}"); } return ow.toString(); } /** * Writes out the java source for a type element. Package directories * will be created as needed as inferred by the type name. * @param srcDir the top level source directory. * @throws IOException if an error occurs. */ public void write(Path srcDir) throws IOException { Files.createDirectories(srcDir); Path outDir = srcDir; if (pkg != null && !pkg.isEmpty()) { String pdir = pkg.replace(".", "/"); outDir = Paths.get(srcDir.toString(), pdir); Files.createDirectories(outDir); } Path filePath = Paths.get(outDir.toString(), clsname + ".java"); tb.writeFile(filePath, this.toString()); } /** * The member builder, this is the base class for all types of members. */ public static abstract class MemberBuilder extends AbstractBuilder { public MemberBuilder(Modifiers modifiers, String name) { super(modifiers, name); } /** * Sets the enclosing type's name. * @param className the enclosing type's name */ @Override void setClassName(String className) { cname = className; } /** * Sets a comment for the element. * * @param comments for any element * @return this builder. */ @Override public MemberBuilder setComments(String... comments) { super.setComments(comments); return this; } /** * Sets a comment for the element. Typically used to set user's * preferences whether an automatic comment is required or no API * comment. * * @param kind of comment, automatic or no comment. * @return this builder. */ @Override public MemberBuilder setComments(Comment.Kind kind) { super.setComments(kind); return this; } /** * Sets a new modifier. * * @param modifiers * @return this builder. */ @Override public MemberBuilder setModifiers(String... modifiers) { super.setModifiers(modifiers); return this; } } /** * The field builder. */ public static class FieldBuilder extends MemberBuilder { private String fieldType; private String value; private static final Pattern FIELD_RE = Pattern.compile("(.*)(\\s*=\\s*)(.*)(;)"); /** * Constructs a field with the modifiers and name of the field. * The builder by default is configured to auto generate the * comments for the field. * @param name of the field */ public FieldBuilder(String name) { super(new Modifiers(), name); this.comments = new Comment(Comment.Kind.AUTO); } /** * Returns a field builder by parsing the string. * ex: public static String myPlayingField; * @param fieldString describing the field. * @return a field builder. */ public static FieldBuilder parse(String fieldString) { String prefix; String value = null; Matcher m = FIELD_RE.matcher(fieldString); if (m.matches()) { prefix = m.group(1).trim(); value = m.group(3).trim(); } else { int end = fieldString.lastIndexOf(';') > 0 ? fieldString.lastIndexOf(';') : fieldString.length(); prefix = fieldString.substring(0, end).trim(); } List list = Stream.of(prefix.split(" ")) .filter(s -> !s.isEmpty()).collect(Collectors.toList()); if (list.size() < 2) { throw new IllegalArgumentException("incorrect field string: " + fieldString); } String name = list.get(list.size() - 1); String fieldType = list.get(list.size() - 2); FieldBuilder fb = new FieldBuilder(name); fb.modifiers.setModifiers(list.subList(0, list.size() - 2)); fb.setFieldType(fieldType); if (value != null) fb.setValue(value); return fb; } /** * Sets the modifiers for this builder. * * @param mods * @return this builder */ public FieldBuilder setModifiers(String mods) { this.modifiers.setModifiers(mods); return this; } /** * Sets the type of the field. * @param fieldType the name of the type. * @return this field builder. */ public FieldBuilder setFieldType(String fieldType) { this.fieldType = fieldType; return this; } public FieldBuilder setValue(String value) { this.value = value; return this; } @Override public String toString() { String indent = " "; OutputWriter ow = new OutputWriter(); switch (comments.kind) { case AUTO: ow.println(indent + "/** Field " + super.name + " in " + super.cname + " */"); break; case INHERIT_DOC: case USER: ow.println(indent + "/** " + comments.toString() + " */"); break; case NO_API_COMMENT: ow.println(indent + "// NO_API_COMMENT"); break; } ow.print(indent + super.modifiers.toString() + " "); ow.print(fieldType + " "); ow.print(super.name); if (value != null) { ow.print(" = " + value); } ow.println(";"); return ow.toString(); } } /** * The method builder. */ public static class MethodBuilder extends MemberBuilder { private final List params; private String returnType; private List body; final static Pattern METHOD_RE = Pattern.compile("(.*)(\\()(.*)(\\))(.*)"); /** * Constructs a method builder. The builder by default is configured * to auto generate the comments for this method. * @param name of the method. */ public MethodBuilder(String name) { super(new Modifiers(), name); comments = new Comment(Comment.Kind.AUTO); params = new ArrayList<>(); body = null; } /** * Returns a method builder by parsing a string which * describes a method. * @param methodString the method description. * @return a method builder. */ public static MethodBuilder parse(String methodString) { Matcher m = METHOD_RE.matcher(methodString); if (!m.matches()) throw new IllegalArgumentException("string does not match: " + methodString); String prefix = m.group(1); String params = m.group(3); String suffix = m.group(5).trim(); if (prefix.length() < 2) { throw new IllegalArgumentException("incorrect method string: " + methodString); } String[] pa = prefix.split(" "); List list = List.of(pa); String name = list.get(list.size() - 1); String returnType = list.get(list.size() - 2); MethodBuilder mb = new MethodBuilder(name); mb.modifiers.setModifiers(list.subList(0, list.size() - 2)); mb.setReturn(returnType); pa = params.split(","); Stream.of(pa).forEach(p -> { p = p.trim(); if (!p.isEmpty()) mb.addParameter(p); }); if (!suffix.isEmpty() || suffix.length() > 1) { mb.setBody(suffix); } return mb; } /** * Sets the modifiers for this builder. * * @param modifiers * @return this builder */ public MethodBuilder setModifiers(String modifiers) { this.modifiers.setModifiers(modifiers); return this; } @Override public MethodBuilder setComments(String... comments) { super.setComments(comments); return this; } @Override public MethodBuilder setComments(Comment.Kind kind) { super.setComments(kind); return this; } /** * Sets a return type for a method. * @param returnType the return type. * @return this method builder. */ public MethodBuilder setReturn(String returnType) { this.returnType = returnType; return this; } /** * Adds a parameter(s) to the method builder. * @param params a pair consisting of type and parameter name. * @return this method builder. */ public MethodBuilder addParameters(Pair... params) { this.params.addAll(List.of(params)); return this; } /** * Adds a parameter to the method builder. * @param type the type of parameter. * @param name the parameter name. * @return this method builder. */ public MethodBuilder addParameter(String type, String name) { this.params.add(new Pair(type, name)); return this; } /** * Adds a parameter to the method builder, by parsing the string. * @param s the parameter description such as "Double voltage" * @return this method builder. */ public MethodBuilder addParameter(String s) { String[] p = s.trim().split(" "); return addParameter(p[0], p[p.length - 1]); } /** * Sets the body of the method, described by the string. * Such as "{", "double i = v/r;", "return i;", "}" * @param body of the methods * @return */ public MethodBuilder setBody(String... body) { if (body == null) { this.body = null; } else { this.body = new ArrayList<>(); this.body.addAll(List.of(body)); } return this; } @Override public String toString() { OutputWriter ow = new OutputWriter(); String indent = " "; switch (comments.kind) { case AUTO: ow.println(indent + "/** Method " + super.name + " in " + super.cname); if (!params.isEmpty()) params.forEach(p -> ow.println(indent + " * @param " + p.second + " a param")); if (returnType != null && !returnType.isEmpty() && !returnType.contains("void")) ow.println(indent + " * @return returns something"); ow.println(indent + " */"); break; case INHERIT_DOC: case USER: ow.println(indent + "/** " + comments.toString() + " */"); break; case NO_API_COMMENT: ow.println(indent + "// NO_API_COMMENT"); break; } ow.print(indent + super.modifiers.toString() + " "); ow.print(returnType + " "); ow.print(super.name + "("); if (!params.isEmpty()) { ListIterator iter = params.listIterator(); while (iter.hasNext()) { Pair p = iter.next(); ow.print(p.first + " " + p.second); if (iter.hasNext()) ow.print(", "); } } ow.print(")"); if (body == null) { ow.println(";"); } else { body.forEach(ow::println); } return ow.toString(); } } //A sample, to test with an IDE. // public static void main(String... args) throws IOException { // ClassBuilder cb = new ClassBuilder(new ToolBox(), "foo.bar.Test"); // cb.addModifiers("public", "abstract", "static") // .addImports("java.io").addImports("java.nio") // .setComments("A comment") // .addImplements("Serialization", "Something") // .setExtends("java.lang.Object") // .addMembers( // FieldBuilder.parse("public int xxx;"), // FieldBuilder.parse("public int yyy = 10;"), // MethodBuilder.parse("public static void main(A a, B b, C c);") // .setComments("CC"), // MethodBuilder.parse("void foo(){//do something}") // // ); // ClassBuilder ic = new ClassBuilder(new ToolBox(), "IC"); // cb.addModifiers( "interface"); // cb.addNestedClasses(ic); // System.out.println(cb.toString()); // cb.write(Paths.get("src-out")); // } }