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}