001    /*
002     * Copyright 2011 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 1.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/ecl1.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     */
016    package org.kuali.rice.devtools.generators.mo;
017    
018    import com.sun.codemodel.ClassType;
019    import com.sun.codemodel.JAnnotationArrayMember;
020    import com.sun.codemodel.JAnnotationUse;
021    import com.sun.codemodel.JBlock;
022    import com.sun.codemodel.JClass;
023    import com.sun.codemodel.JCodeModel;
024    import com.sun.codemodel.JConditional;
025    import com.sun.codemodel.JDefinedClass;
026    import com.sun.codemodel.JDocComment;
027    import com.sun.codemodel.JExpr;
028    import com.sun.codemodel.JFieldRef;
029    import com.sun.codemodel.JFieldVar;
030    import com.sun.codemodel.JInvocation;
031    import com.sun.codemodel.JMethod;
032    import com.sun.codemodel.JMod;
033    import com.sun.codemodel.JType;
034    import com.sun.codemodel.JVar;
035    import com.sun.codemodel.writer.SingleStreamCodeWriter;
036    import org.apache.commons.lang.builder.EqualsBuilder;
037    import org.apache.commons.lang.builder.HashCodeBuilder;
038    import org.apache.commons.lang.builder.ToStringBuilder;
039    import org.kuali.rice.core.api.CoreConstants;
040    import org.kuali.rice.core.api.mo.AbstractDataTransferObject;
041    import org.kuali.rice.core.api.mo.ModelBuilder;
042    
043    import javax.xml.bind.annotation.XmlAccessType;
044    import javax.xml.bind.annotation.XmlAccessorType;
045    import javax.xml.bind.annotation.XmlAnyElement;
046    import javax.xml.bind.annotation.XmlElement;
047    import javax.xml.bind.annotation.XmlRootElement;
048    import javax.xml.bind.annotation.XmlType;
049    import java.io.ByteArrayOutputStream;
050    import java.io.Serializable;
051    import java.lang.reflect.Method;
052    import java.util.ArrayList;
053    import 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     */
062    public 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    }