001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.devtools.generators.mo;
017
018import com.sun.codemodel.ClassType;
019import com.sun.codemodel.JAnnotationArrayMember;
020import com.sun.codemodel.JAnnotationUse;
021import com.sun.codemodel.JBlock;
022import com.sun.codemodel.JClass;
023import com.sun.codemodel.JCodeModel;
024import com.sun.codemodel.JConditional;
025import com.sun.codemodel.JDefinedClass;
026import com.sun.codemodel.JDocComment;
027import com.sun.codemodel.JExpr;
028import com.sun.codemodel.JFieldRef;
029import com.sun.codemodel.JFieldVar;
030import com.sun.codemodel.JInvocation;
031import com.sun.codemodel.JMethod;
032import com.sun.codemodel.JMod;
033import com.sun.codemodel.JType;
034import com.sun.codemodel.JVar;
035import com.sun.codemodel.writer.SingleStreamCodeWriter;
036import org.apache.commons.lang.builder.EqualsBuilder;
037import org.apache.commons.lang.builder.HashCodeBuilder;
038import org.apache.commons.lang.builder.ToStringBuilder;
039import org.kuali.rice.core.api.CoreConstants;
040import org.kuali.rice.core.api.mo.AbstractDataTransferObject;
041import org.kuali.rice.core.api.mo.ModelBuilder;
042
043import javax.xml.bind.annotation.XmlAccessType;
044import javax.xml.bind.annotation.XmlAccessorType;
045import javax.xml.bind.annotation.XmlAnyElement;
046import javax.xml.bind.annotation.XmlElement;
047import javax.xml.bind.annotation.XmlRootElement;
048import javax.xml.bind.annotation.XmlType;
049import java.io.ByteArrayOutputStream;
050import java.io.Serializable;
051import java.lang.reflect.Method;
052import java.util.ArrayList;
053import java.util.List;
054
055/**
056 * This is a simple utility class which generates an "immutable" object complete with a builder based on a supplied
057 * contract interface definition.
058 * 
059 * @author Kuali Rice Team (rice.collab@kuali.org)
060 *
061 */
062public class ImmutableJaxbGenerator {
063
064        public static void main(String[] args) throws Exception {
065                
066                if (args.length > 2 || args.length < 1) {
067                        System.err.println("There should be two arguments defined as follows:\n" + 
068                                        "     1. Fully qualified class name of a 'contract' interface\n" + 
069                                        "     2. [Optional] Fully qualified class name of the class to generate.  If not specified, will use the name of the contract interface class and remove \"Contract\" from the end of it.\n");
070                        System.exit(1);
071                }
072                
073                // argument one should be a fully qualified class name of a "contract" interface
074                String contractInterfaceName = args[0];
075                
076                String className = null;
077                // argument two should be the fully qualified class name of the class to generate
078                if (args.length == 2) {
079                        className = args[1];
080                } else {
081                        if (!contractInterfaceName.endsWith("Contract")) {
082                                throw new IllegalArgumentException("If not explicitly specifying target classname, then contract class name must end with 'Contract'");
083                        }
084                        className = contractInterfaceName.substring(0, contractInterfaceName.lastIndexOf("Contract"));
085                }
086                
087                Generator generator = new Generator(contractInterfaceName, className);
088                generator.generate();
089        }
090        
091        public static class Generator {
092                
093                private final String contractInterfaceName;
094                private final String className;
095                private final JCodeModel codeModel;
096                
097                public Generator(String contractInterfaceName, String className) {
098                        this.contractInterfaceName = contractInterfaceName;
099                        this.className = className;
100                        this.codeModel = new JCodeModel();
101                }
102                
103                public void generate() throws Exception {
104                        byte[] javaCode = generateJava();
105                        System.out.println(new String(javaCode));
106                }
107                
108                private byte[] generateJava() throws Exception {
109                        
110                        JDefinedClass classModel = codeModel._class(JMod.PUBLIC | JMod.FINAL, className, ClassType.CLASS);
111                        Class<?> contractInterface = Class.forName(contractInterfaceName);
112                        classModel._implements(contractInterface);
113            classModel._extends(AbstractDataTransferObject.class);
114                        
115                        List<FieldModel> fields = determineFields(contractInterface);
116                        
117                        renderConstantsClass(classModel);
118                        renderElementsClass(classModel, fields);
119                        renderClassLevelAnnotations(classModel, fields);
120                        renderFields(classModel, fields);
121                        renderFutureElementsField(classModel);                  
122                        renderPrivateJaxbConstructor(classModel, fields);
123                        renderBuilderConstructor(classModel, fields);
124                        renderGetters(classModel, fields);
125                        renderBuilderClass(classModel, fields, contractInterface);
126
127                        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
128                        codeModel.build(new SingleStreamCodeWriter(outputStream));
129                        return outputStream.toByteArray();
130                        
131                }
132                
133                private List<FieldModel> determineFields(Class<?> contractInterface) throws Exception {
134                        List<FieldModel> fieldModels = new ArrayList<FieldModel>();
135                        
136                        Method[] methods = contractInterface.getMethods();
137                        for (Method method : methods) {
138                                String methodName = method.getName();
139                                String fieldName = null;
140                                if (method.getReturnType() != Void.class && method.getParameterTypes().length == 0) {
141                                        if (methodName.startsWith("get")) {
142                                                fieldName = Util.toLowerCaseFirstLetter(methodName.substring(3));
143                                        } else if (methodName.startsWith("is")) {
144                                                fieldName = Util.toLowerCaseFirstLetter(methodName.substring(2));
145                                        } else {
146                                                continue;
147                                        }
148                                        fieldModels.add(new FieldModel(fieldName, method.getReturnType()));
149                                }
150                        }
151                        
152                        return fieldModels;
153                }
154                
155                private void renderConstantsClass(JDefinedClass classModel) throws Exception {
156                        
157                        // define constants class
158                        JDefinedClass constantsClass = classModel._class(JMod.STATIC, Util.CONSTANTS_CLASS_NAME);
159                        
160                        // generate the javadoc on the top of the Constants class
161                        JDocComment javadoc = constantsClass.javadoc();
162                        javadoc.append(Util.CONSTANTS_CLASS_JAVADOC);
163                        
164                        // render root element name
165                        JFieldVar rootElementField = constantsClass.field(JMod.FINAL | JMod.STATIC, String.class, Util.ROOT_ELEMENT_NAME_FIELD);
166                        rootElementField.init(JExpr.lit(Util.toLowerCaseFirstLetter(classModel.name())));
167                        
168                        // render type name
169                        JFieldVar typeNameField = constantsClass.field(JMod.FINAL | JMod.STATIC, String.class, Util.TYPE_NAME_FIELD);
170                        typeNameField.init(JExpr.lit(classModel.name() + Util.TYPE_NAME_SUFFIX));
171            
172                }
173                
174                private void renderElementsClass(JDefinedClass classModel, List<FieldModel> fields) throws Exception {
175                        
176                        // define constants class
177                        JDefinedClass elementsClass = classModel._class(JMod.STATIC, Util.ELEMENTS_CLASS_NAME);
178                        
179                        // generate the javadoc on the top of the Elements class
180                        JDocComment javadoc = elementsClass.javadoc();
181                        javadoc.append(Util.ELEMENTS_CLASS_JAVADOC);
182                        
183                        // go through each field and create a corresponding constant
184                        for (FieldModel fieldModel : fields) {
185                                if (Util.isCommonElement(fieldModel.fieldName)) {
186                                        continue;
187                                }
188                                JFieldVar elementFieldVar = elementsClass.field(JMod.FINAL | JMod.STATIC, String.class, Util.toConstantsVariable(fieldModel.fieldName));
189                                elementFieldVar.init(JExpr.lit(fieldModel.fieldName));
190                        }
191                }
192                
193                private void renderClassLevelAnnotations(JDefinedClass classModel, List<FieldModel> fields) throws Exception {
194                        JFieldRef constantsClass = classModel.staticRef(Util.CONSTANTS_CLASS_NAME);
195                        JFieldRef elementsClass = classModel.staticRef(Util.ELEMENTS_CLASS_NAME);
196                        JClass coreConstants = codeModel.ref(CoreConstants.class);
197                        JFieldRef commonElementsRef = coreConstants.staticRef("CommonElements");
198                        
199                        // XmlRootElement
200                        JAnnotationUse rootElementAnnotation = classModel.annotate(XmlRootElement.class);
201                        rootElementAnnotation.param("name", constantsClass.ref(Util.ROOT_ELEMENT_NAME_FIELD));
202                        
203                        // XmlAccessorType
204                        JAnnotationUse xmlAccessorTypeAnnotation = classModel.annotate(XmlAccessorType.class);
205                        xmlAccessorTypeAnnotation.param("value", XmlAccessType.NONE);
206                        
207                        // XmlType
208                        JAnnotationUse xmlTypeAnnotation = classModel.annotate(XmlType.class);
209                        xmlTypeAnnotation.param("name", constantsClass.ref(Util.TYPE_NAME_FIELD));
210                        JAnnotationArrayMember propOrderMember = xmlTypeAnnotation.paramArray("propOrder");
211                        for (FieldModel field : fields) {
212                                if (Util.isCommonElement(field.fieldName)) {
213                                        propOrderMember.param(commonElementsRef.ref(Util.toConstantsVariable(field.fieldName)));
214                                } else {
215                                        propOrderMember.param(elementsClass.ref(Util.toConstantsVariable(field.fieldName)));
216                                }
217                        }
218                        propOrderMember.param(commonElementsRef.ref("FUTURE_ELEMENTS"));
219                }
220                
221                private void renderFields(JDefinedClass classModel, List<FieldModel> fields) {
222                        for (FieldModel fieldModel : fields) {
223                                renderField(classModel, fieldModel);
224                        }
225                }
226                
227                private void renderField(JDefinedClass classModel, FieldModel fieldModel) {
228                        JFieldVar field = classModel.field(JMod.PRIVATE | JMod.FINAL, fieldModel.fieldType, fieldModel.fieldName);
229                        JAnnotationUse annotation = field.annotate(XmlElement.class);
230                        if (Util.isCommonElement(fieldModel.fieldName)) {
231                                JClass coreConstants = codeModel.ref(CoreConstants.class);
232                                JFieldRef commonElementsRef = coreConstants.staticRef("CommonElements");
233                                annotation.param("name", commonElementsRef.ref(Util.toConstantsVariable(fieldModel.fieldName)));
234                        } else {
235                                JClass elementsClass = codeModel.ref(Util.ELEMENTS_CLASS_NAME);
236                                JFieldRef fieldXmlNameRef = elementsClass.staticRef(Util.toConstantsVariable(fieldModel.fieldName));
237                                annotation.param("name", fieldXmlNameRef);
238                        }
239                        annotation.param("required", false);
240                }
241                
242                private void renderFutureElementsField(JDefinedClass classModel) throws Exception {
243                        JType collectionType = codeModel.parseType("java.util.Collection<org.w3c.dom.Element>");
244                        JFieldVar field = classModel.field(JMod.PRIVATE | JMod.FINAL, collectionType, "_futureElements");
245                        field.init(JExpr._null());
246                        JAnnotationUse annotation = field.annotate(SuppressWarnings.class);
247                        annotation.param("value", "unused");
248                        field.annotate(XmlAnyElement.class);
249                }
250                
251                private void renderPrivateJaxbConstructor(JDefinedClass classModel, List<FieldModel> fields) {
252                        JMethod method = classModel.constructor(JMod.PRIVATE);
253                        JBlock body = method.body();
254                        for (FieldModel fieldModel : fields) {
255                                body.directStatement("this." + fieldModel.fieldName + " = " + getInitString(fieldModel.fieldType) + ";");
256                        }
257                        method.javadoc().add("Private constructor used only by JAXB.");
258                }
259
260        private String getInitString(Class<?> clazz) {
261            if (clazz == Boolean.TYPE) {
262                return "false";
263            } else if (clazz == Character.TYPE) {
264                return "'\\\\u0000'";
265            } else if (clazz == Long.TYPE) {
266                return "0L";
267            } else if (clazz == Float.TYPE) {
268                return "0.0F";
269            } else if (clazz == Double.TYPE) {
270                return "0.0D";
271            } else if (clazz == Byte.TYPE || clazz == Short.TYPE || clazz == Integer.TYPE) {
272                return "0";
273            } else {
274                return "null";
275            }
276        }
277                
278                private void renderBuilderConstructor(JDefinedClass classModel, List<FieldModel> fields) {
279                        JMethod method = classModel.constructor(JMod.PRIVATE);
280                        method.param(codeModel.ref("Builder"), "builder");
281                        JBlock body = method.body();
282                        for (FieldModel fieldModel : fields) {
283                                body.directStatement("this." + fieldModel.fieldName + " = builder." + Util.generateGetter(fieldModel.fieldName, isBoolean(fieldModel.fieldType)) + ";");
284                        }
285                }
286                
287                private void renderGetters(JDefinedClass classModel, List<FieldModel> fields) {
288            for (FieldModel fieldModel : fields) {
289                JMethod getterMethod = classModel.method(JMod.PUBLIC, fieldModel.fieldType, Util.generateGetterName(fieldModel.fieldName, isBoolean(fieldModel.fieldType)));
290                                JBlock methodBody = getterMethod.body();
291                                methodBody.directStatement("return this." + fieldModel.fieldName + ";");
292                                getterMethod.annotate(Override.class);
293                        }
294                }
295
296        private boolean isBoolean(Class<?> clazz) {
297            return clazz == Boolean.TYPE || clazz == Boolean.class;
298        }
299                
300                private void renderBuilderClass(JDefinedClass classModel, List<FieldModel> fields, Class<?> contractInterface) throws Exception {
301                        
302                        // define constants class
303                        JDefinedClass builderClass = classModel._class(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, Util.BUILDER_CLASS_NAME);
304                        
305                        // create a literal version of the Builder class so that the code generator won't pre-pend Builder class references with outermost class
306                        JClass literalBuilderClass = codeModel.ref("Builder");
307                        
308                        // generate the javadoc on the top of the Elements class
309                        JDocComment javadoc = builderClass.javadoc();
310                        javadoc.append(Util.generateBuilderJavadoc(classModel.name(), contractInterface.getSimpleName()));
311                        
312                        builderClass._implements(contractInterface);
313                        builderClass._implements(ModelBuilder.class);
314                        builderClass._implements(Serializable.class);
315                        
316                        // render the builder fields
317                        for (FieldModel fieldModel : fields) {
318                                builderClass.field(JMod.PRIVATE, fieldModel.fieldType, fieldModel.fieldName);
319                        }
320                        
321                        // render default empty constructor for builder
322                        JMethod constructor = builderClass.constructor(JMod.PRIVATE);
323                        constructor.body().directStatement("// TODO modify this constructor as needed to pass any required values and invoke the appropriate 'setter' methods");
324                
325                        renderBuilderDefaultCreate(builderClass, literalBuilderClass);
326                        renderBuilderCreateContract(builderClass, literalBuilderClass, fields, contractInterface);
327                        renderBuild(builderClass);
328                        renderGetters(builderClass, fields);
329                        renderSetters(builderClass, fields);
330                }
331                
332                private void renderBuilderDefaultCreate(JDefinedClass builderClass, JClass literalBuilderClass) {
333                        JMethod createMethod = builderClass.method(JMod.PUBLIC | JMod.STATIC, literalBuilderClass, "create");
334                        JBlock createMethodBody = createMethod.body();
335                        createMethodBody.directStatement("// TODO modify as needed to pass any required values and add them to the signature of the 'create' method");
336                        createMethodBody.directStatement("return new Builder();");
337                }
338                
339                private void renderBuilderCreateContract(JDefinedClass builderClass, JClass literalBuilderClass, List<FieldModel> fields, Class<?> contractInterface) {
340                        JMethod createContractMethod = builderClass.method(JMod.PUBLIC | JMod.STATIC, literalBuilderClass, "create");
341                        JVar contractParam = createContractMethod.param(contractInterface, "contract");
342                        JBlock body = createContractMethod.body();
343                        JConditional nullContractCheck = body._if(contractParam.eq(JExpr._null()));
344                        nullContractCheck._then().directStatement("throw new IllegalArgumentException(\"contract was null\");");
345                        body.directStatement("// TODO if create() is modified to accept required parameters, this will need to be modified");
346                        body.directStatement("Builder builder = create();");
347                        for (FieldModel fieldModel : fields) {
348                                String fieldName = fieldModel.fieldName;
349                                body.directStatement("builder." + Util.generateSetter(fieldName, "contract." + Util.generateGetter(fieldName, isBoolean(fieldModel.fieldType))) + ";");
350                        }
351                        body.directStatement("return builder;");
352                }
353                
354                private void renderBuild(JDefinedClass builderClass) {
355                        JMethod buildMethod = builderClass.method(JMod.PUBLIC, builderClass.outer(), "build");
356                        buildMethod.body().directStatement("return new " + builderClass.outer().name() + "(this);");
357                }
358                
359                private void renderSetters(JDefinedClass builderClass, List<FieldModel> fields) {
360                        for (FieldModel fieldModel : fields) {
361                                String fieldName = fieldModel.fieldName;
362                                JMethod setterMethod = builderClass.method(JMod.PUBLIC, codeModel.VOID, Util.generateSetterName(fieldName));
363                                setterMethod.param(fieldModel.fieldType, fieldName);
364                                setterMethod.body().directStatement("// TODO add validation of input value if required and throw IllegalArgumentException if needed");
365                                setterMethod.body().directStatement("this." + fieldName + " = " + fieldName + ";");
366                        }
367                }
368
369        }
370        
371        private static class FieldModel {
372                
373                private final String fieldName;
374                private final Class<?> fieldType;
375                
376                private FieldModel(String fieldName, Class<?> fieldType) {
377                        this.fieldName = fieldName;
378                        this.fieldType = fieldType;
379                }
380                                
381        }
382        
383}