001 /* 002 * Copyright 2011 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.osedu.org/licenses/ECL-2.0 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.student.datadictionary.util; 017 018 import java.io.File; 019 import java.io.FileNotFoundException; 020 import java.io.FileOutputStream; 021 import java.io.PrintStream; 022 import java.text.BreakIterator; 023 import java.util.Collections; 024 import java.util.Date; 025 import java.util.HashMap; 026 import java.util.LinkedHashSet; 027 import java.util.List; 028 import java.util.Map; 029 import java.util.Set; 030 import java.util.Stack; 031 import org.apache.commons.lang.StringEscapeUtils; 032 033 import org.kuali.student.contract.model.MessageStructure; 034 import org.kuali.student.contract.model.ServiceContractModel; 035 import org.kuali.student.contract.model.XmlType; 036 import org.kuali.student.contract.model.util.ModelFinder; 037 import org.kuali.student.contract.writer.XmlWriter; 038 039 /** 040 * 041 * @author nwright 042 */ 043 public class KradDictionaryCreator { 044 045 private ServiceContractModel model; 046 private ModelFinder finder; 047 private String directory; 048 private String className; 049 private XmlType xmlType; 050 private XmlWriter gwriter; 051 private XmlWriter mwriter; 052 private List<MessageStructure> messageStructures; 053 private boolean writeManual; 054 private boolean writeGenerated; 055 056 public KradDictionaryCreator(String directory, 057 ServiceContractModel model, String className, boolean writeManual, boolean writeGenerated) { 058 this.directory = directory; 059 this.model = model; 060 this.finder = new ModelFinder(this.model); 061 this.className = className; 062 this.xmlType = this.finder.findXmlType(className); 063 if (xmlType == null) { 064 throw new IllegalArgumentException(className); 065 } 066 this.messageStructures = this.finder.findMessageStructures(className); 067 this.writeManual = writeManual; 068 this.writeGenerated = writeGenerated; 069 // if (this.messageStructures.isEmpty()) { 070 // throw new IllegalStateException(className); 071 // } 072 } 073 074 public void write() { 075 this.initXmlWriters(); 076 if (writeGenerated) { 077 this.writeSpringHeaderOpen(gwriter); 078 this.writeWarning(gwriter); 079 this.writeGeneratedImports(gwriter); 080 this.writeGeneratedObjectStructure(gwriter); 081 this.writeSpringHeaderClose(gwriter); 082 } 083 if (this.writeManual) { 084 this.writeSpringHeaderOpen(mwriter); 085 this.writeNote(mwriter); 086 this.writeManualImports(mwriter); 087 this.writeManualObjectStructure(mwriter); 088 this.writeSpringHeaderClose(mwriter); 089 } 090 } 091 092 private void initXmlWriters() { 093 String generatedFileName = "ks-" + initUpper(className) + "-dictionary-generated.xml"; 094 String manualFileName = "ks-" + initUpper(className) + "-dictionary.xml"; 095 096 File dir = new File(this.directory); 097 //System.out.indentPrintln ("Writing java class: " + fileName + " to " + dir.getAbsolutePath ()); 098 099 if (!dir.exists()) { 100 if (!dir.mkdirs()) { 101 throw new IllegalStateException("Could not create directory " 102 + this.directory); 103 } 104 } 105 106 if (writeGenerated) { 107 String dirStr = this.directory + "/generated"; 108 File dirFile = new File(dirStr); 109 if (!dirFile.exists()) { 110 if (!dirFile.mkdirs()) { 111 throw new IllegalStateException("Could not create directory " 112 + dirStr); 113 } 114 } 115 try { 116 PrintStream out = new PrintStream(new FileOutputStream(dirStr + "/" + generatedFileName, false)); 117 this.gwriter = new XmlWriter(out, 0); 118 } catch (FileNotFoundException ex) { 119 throw new IllegalStateException(ex); 120 } 121 } 122 if (this.writeManual) { 123 String dirStr = this.directory + "/manual"; 124 File dirFile = new File(dirStr); 125 if (!dirFile.exists()) { 126 if (!dirFile.mkdirs()) { 127 throw new IllegalStateException("Could not create directory " 128 + dirStr); 129 } 130 } 131 try { 132 PrintStream out = new PrintStream(new FileOutputStream(dirStr + "/" + manualFileName, false)); 133 this.mwriter = new XmlWriter(out, 0); 134 } catch (FileNotFoundException ex) { 135 throw new IllegalStateException(ex); 136 } 137 } 138 } 139 140 private static String initLower(String str) { 141 if (str == null) { 142 return null; 143 } 144 if (str.length() == 0) { 145 return str; 146 } 147 if (str.length() == 1) { 148 return str.toLowerCase(); 149 } 150 return str.substring(0, 1).toLowerCase() + str.substring(1); 151 } 152 153 private static String initUpper(String str) { 154 if (str == null) { 155 return null; 156 } 157 if (str.length() == 0) { 158 return str; 159 } 160 if (str.length() == 1) { 161 return str.toUpperCase(); 162 } 163 return str.substring(0, 1).toUpperCase() + str.substring(1); 164 } 165 166 private void writeSpringHeaderClose(XmlWriter out) { 167 out.decrementIndent(); 168 out.indentPrintln("</beans>"); 169 } 170 171 private void writeSpringHeaderOpen(XmlWriter out) { 172 out.indentPrintln("<!--"); 173 out.indentPrintln(" Copyright 2011 The Kuali Foundation"); 174 out.println(""); 175 out.indentPrintln(" Licensed under the Educational Community License, Version 2.0 (the \"License\");"); 176 out.indentPrintln(" you may not use this file except in compliance with the License."); 177 out.indentPrintln(" You may obtain a copy of the License at"); 178 out.indentPrintln(""); 179 out.indentPrintln(" http://www.opensource.org/licenses/ecl2.php"); 180 out.println(""); 181 out.indentPrintln(" Unless required by applicable law or agreed to in writing, software"); 182 out.indentPrintln(" distributed under the License is distributed on an \"AS IS\" BASIS,"); 183 out.indentPrintln(" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied."); 184 out.indentPrintln(" See the License for the specific language governing permissions and"); 185 out.indentPrintln(" limitations under the License."); 186 out.indentPrintln("-->"); 187 out.indentPrintln("<beans xmlns=\"http://www.springframework.org/schema/beans\""); 188 out.indentPrintln("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); 189 out.indentPrintln("xsi:schemaLocation=\"" 190 + "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd" + "\">"); 191 out.println(""); 192 out.incrementIndent(); 193 } 194 195 private void writeWarning(XmlWriter out) { 196 out.println(""); 197 out.indentPrintln("<!-- ********************************************************"); 198 out.incrementIndent(); 199 out.indentPrintln(" WARNING "); 200 out.indentPrintln(" DO NOT UPDATE THIS FILE MANUALLY"); 201 out.indentPrintln("This dictionary file was automatically generated"); 202 out.indentPrintln("The DictionaryGeneratorMojo reads the service contract "); 203 out.indentPrintln("and creates these ks-XXXX-dictionary-generated.xml files."); 204 out.println(""); 205 out.indentPrintln("If this file is out of sync with the contract re-run the mojo."); 206 out.println(""); 207 out.indentPrintln("To add additional constraints or change these default values (perhaps"); 208 out.indentPrintln("because the generator is not perfect) please update the corresponding "); 209 out.indentPrintln("ks-XXXX-dictionary.xml instead of this one."); 210 out.decrementIndent(); 211 out.indentPrintln("************************************************************* -->"); 212 } 213 214 private void writeNote(XmlWriter out) { 215 out.println(""); 216 out.indentPrintln("<!-- ********************************************************"); 217 out.incrementIndent(); 218 out.indentPrintln(" NOTE"); 219 out.indentPrintln(" THIS FILE WAS INTENDED TO BE MODIFIED"); 220 out.println(""); 221 out.indentPrintln("While this file was originally generated, it"); 222 out.indentPrintln("was intended to be subsequently modified by hand."); 223 out.indentPrintln("It imports a corresponding ks-XXXX-dictionary-generated.xml file, "); 224 out.indentPrintln("that was also automatically generated by the ContractDocMojo."); 225 out.indentPrintln("This file gives you the ability to layer on addiditional definitions and constrints"); 226 out.indentPrintln("that are not/cannot be generated simply by reading the service contract."); 227 out.println(""); 228 out.indentPrintln("The goal of this file is to be able to re-generate the corresponding"); 229 out.indentPrintln("ks-XXXX-dictionary-generated.xml file without affecting these manually entered additions"); 230 out.indentPrintln("that are encoded here."); 231 out.decrementIndent(); 232 out.indentPrintln("************************************************************* -->"); 233 } 234 235 private void writeGeneratedImports(XmlWriter out) { 236 // don't actually generate imports because it slows down the springbean generation 237 out.writeCommentBox("The following file is required for this file to load:\n ks-base-dictionary.xml\nplus any of its dependencies"); 238 out.indentPrintln("<import resource=\"classpath:ks-base-dictionary.xml\"/>"); 239 // TODO: only write out the ones that are used in this structure 240 // out.indentPrintln("<import resource=\"classpath:ks-RichTextInfo-dictionary.xml\"/>"); 241 // out.indentPrintln("<import resource=\"classpath:ks-MetaInfo-dictionary.xml\"/>"); 242 } 243 244 private void writeManualImports(XmlWriter out) { 245 out.writeComment("The following file gets generated during the build and gets put into the target/classes directory"); 246 out.indentPrintln("<import resource=\"classpath:ks-" + initUpper(className) + "-dictionary-generated.xml\"/>"); 247 Set<String> imports = this.getComplexSubObjectsThatAreLists(); 248 if (!imports.isEmpty()) { 249 out.writeComment("TODO: remove these once the jira about lists of complex objects gets fixed"); 250 for (String impName : imports) { 251 out.indentPrintln("<import resource=\"classpath:ks-" + initUpper(impName) + "-dictionary.xml\"/>"); 252 } 253 } 254 } 255 256 private Set<String> getComplexSubObjectsThatAreLists() { 257 Set<String> list = new LinkedHashSet(); 258 for (MessageStructure ms : this.messageStructures) { 259 switch (this.calculateCategory(ms)) { 260 case LIST_OF_COMPLEX: 261 String classNameToAdd = this.stripListOffEnd(ms.getType()); 262 // Avoid recursive calls 263 if (!classNameToAdd.equalsIgnoreCase(className)) { 264 list.add(classNameToAdd); 265 } 266 } 267 } 268 return list; 269 } 270 271 private String stripListOffEnd(String name) { 272 if (name.endsWith("List")) { 273 return name.substring(0, name.length() - "List".length()); 274 } 275 return name; 276 } 277 278 private String calcDataObjectClass(XmlType xmlType) { 279 // this is those packages that are not included in the sources for Enroll-API for the model 280 // so the package is null but the name is the full package spec 281 if (xmlType.getJavaPackage() == null || xmlType.getJavaPackage().isEmpty()) { 282 return xmlType.getName(); 283 } 284 return xmlType.getJavaPackage() + "." + initUpper(xmlType.getName()); 285 } 286 287 private void writeGeneratedObjectStructure(XmlWriter out) { 288 //Step 1, create the abstract structure 289 out.println(""); 290 out.indentPrintln("<!-- " + className + "-->"); 291 out.indentPrintln("<bean id=\"" + initUpper(className) + "-generated\" abstract=\"true\" parent=\"DataObjectEntry\">"); 292 out.incrementIndent(); 293 writeProperty("name", initLower(className), out); 294 writeProperty("dataObjectClass", calcDataObjectClass(xmlType), out); 295 writeProperty("objectLabel", calcObjectLabel(), out); 296 writePropertyValue("objectDescription", xmlType.getDesc(), out); 297 String titleAttribute = calcTitleAttribute(); 298 if (titleAttribute != null) { 299 writeProperty("titleAttribute", titleAttribute, out); 300 } 301 out.indentPrintln("<property name=\"primaryKeys\">"); 302 List<String> pks = calcPrimaryKeys(); 303 if (pks != null && !pks.isEmpty()) { 304 out.incrementIndent(); 305 out.indentPrintln("<list>"); 306 out.incrementIndent(); 307 for (String pk : pks) { 308 addValue(pk); 309 } 310 out.decrementIndent(); 311 out.indentPrintln("</list>"); 312 out.decrementIndent(); 313 } 314 out.indentPrintln("</property>"); 315 316 this.writeAllGeneratedAttributeRefBeans(className, null, new Stack<String>(), this.messageStructures, out); 317 318 out.indentPrintln("</bean>"); 319 320 //Step 2, loop through attributes 321 this.writeGeneratedAttributeDefinitions(className, null, new Stack<String>(), this.messageStructures, out); 322 } 323 324 private void writeAllGeneratedAttributeRefBeans(String currentClassName, 325 String parentName, 326 Stack<String> parents, 327 List<MessageStructure> fields, 328 XmlWriter out) { 329 if (parents.contains(currentClassName)) { 330 return; 331 } 332 out.println(""); 333 out.indentPrintln("<property name=\"attributes\">"); 334 out.incrementIndent(); 335 out.indentPrintln("<list>"); 336 out.incrementIndent(); 337 this.writeGeneratedAttributeRefBeans(currentClassName, parentName, parents, fields, out, Category.PRIMITIVE); 338 out.decrementIndent(); 339 out.indentPrintln("</list>"); 340 out.decrementIndent(); 341 out.indentPrintln("</property>"); 342 343 out.println(""); 344 out.indentPrintln("<property name=\"complexAttributes\">"); 345 out.incrementIndent(); 346 out.indentPrintln("<list>"); 347 out.incrementIndent(); 348 this.writeGeneratedAttributeRefBeans(currentClassName, parentName, parents, fields, out, Category.COMPLEX); 349 out.decrementIndent(); 350 out.indentPrintln("</list>"); 351 out.decrementIndent(); 352 out.indentPrintln("</property>"); 353 354 out.println(""); 355 out.indentPrintln("<property name=\"collections\">"); 356 out.incrementIndent(); 357 out.indentPrintln("<list>"); 358 out.incrementIndent(); 359 this.writeGeneratedAttributeRefBeans(currentClassName, parentName, parents, fields, out, Category.LIST_OF_COMPLEX); 360 out.decrementIndent(); 361 out.indentPrintln("</list>"); 362 out.decrementIndent(); 363 out.indentPrintln("</property>"); 364 out.decrementIndent(); 365 } 366 367 private void addValue(String value) { 368 gwriter.indentPrintln("<value>" + value + "</value>"); 369 } 370 371 private String calcObjectLabel() { 372 String label = this.className; 373 if (label.endsWith("Info")) { 374 label = label.substring(0, label.length() - "Info".length()); 375 } 376 label = initUpper(label); 377 return splitCamelCase(label); 378 } 379 380 // got this from http://stackoverflow.com/questions/2559759/how-do-i-convert-camelcase-into-human-readable-names-in-java 381 private static String splitCamelCase(String s) { 382 if (s == null) { 383 return null; 384 } 385 return s.replaceAll( 386 String.format("%s|%s|%s", 387 "(?<=[A-Z])(?=[A-Z][a-z])", 388 "(?<=[^A-Z])(?=[A-Z])", 389 "(?<=[A-Za-z])(?=[^A-Za-z])"), 390 " "); 391 } 392 393 private enum Category { 394 395 PRIMITIVE, COMPLEX, LIST_OF_COMPLEX, LIST_OF_PRIMITIVE, DYNAMIC_ATTRIBUTE 396 }; 397 398 private Category calculateCategory(MessageStructure ms) { 399 if (ms.getShortName().equals("attributes")) { 400 return Category.DYNAMIC_ATTRIBUTE; 401 } 402 String childXmlTypeName = this.stripListOffEnd(ms.getType()); 403 XmlType childXmlType = this.finder.findXmlType(childXmlTypeName); 404 if (childXmlType == null) { 405 throw new IllegalStateException(childXmlTypeName); 406 } 407 if (ms.getType().endsWith("List")) { 408 if (childXmlType.getPrimitive().equalsIgnoreCase(XmlType.COMPLEX)) { 409 return Category.LIST_OF_COMPLEX; 410 } 411 return Category.LIST_OF_PRIMITIVE; 412 } 413 if (childXmlType.getPrimitive().equalsIgnoreCase(XmlType.COMPLEX)) { 414 return Category.COMPLEX; 415 } 416 return Category.PRIMITIVE; 417 } 418 419 private void writeGeneratedAttributeRefBeans(String currentClass, 420 String parentName, 421 Stack<String> parents, 422 List<MessageStructure> fields, 423 XmlWriter out, 424 Category filter) { 425 if (parents.contains(currentClass)) { 426 return; 427 } 428 for (MessageStructure ms : fields) { 429 Category category = this.calculateCategory(ms); 430 if (!category.equals(filter)) { 431 continue; 432 } 433 String childXmlTypeName = this.stripListOffEnd(ms.getType()); 434 XmlType childXmlType = this.finder.findXmlType(childXmlTypeName); 435 if (childXmlType == null) { 436 throw new IllegalStateException(childXmlTypeName); 437 } 438 String pathName = calcPathName(parentName, ms); 439 String beanName = calcBeanName(pathName); 440 // TODO: change this once they fix the list of complex jira 441 // if (filter.equals(Category.LIST_OF_COMPLEX)) { 442 // beanName = initUpper(childXmlTypeName); 443 // } 444 out.indentPrintln("<ref bean=\"" + beanName + "\"/>"); 445 // 446 // // Add complex sub-types fields 447 // switch (category) { 448 // case COMPLEX: 449 // case LIST_OF_COMPLEX: 450 // parents.push(currentClass); 451 // List<MessageStructure> childFields = this.finder.findMessageStructures(childXmlTypeName); 452 // writeGeneratedAttributeRefBeans(childXmlTypeName, pathName, parents, childFields, out, filter); 453 // parents.pop(); 454 // } 455 } 456 } 457 458 private void writeGeneratedAttributeDefinitions(String currentClassName, 459 String parentName, 460 Stack<String> parents, 461 List<MessageStructure> fields, 462 XmlWriter out) { 463 if (parents.contains(currentClassName)) { 464 return; 465 } 466 for (MessageStructure ms : fields) { 467 Category category = this.calculateCategory(ms); 468 switch (category) { 469 case DYNAMIC_ATTRIBUTE: 470 continue; 471 } 472 String pathName = calcPathName(parentName, ms); 473 String beanName = calcBeanName(pathName); 474 String childXmlTypeName = this.stripListOffEnd(ms.getType()); 475 XmlType childXmlType = this.finder.findXmlType(childXmlTypeName); 476 if (childXmlType == null) { 477 throw new IllegalStateException(childXmlTypeName); 478 } 479 writeGeneratedAttributeDefinition(currentClassName, parentName, parents, ms, out); 480 481 // Add complex sub-types fields 482 switch (category) { 483 case COMPLEX: 484 // case LIST_OF_COMPLEX: 485 parents.push(currentClassName); 486 List<MessageStructure> childFields = this.finder.findMessageStructures(childXmlTypeName); 487 writeGeneratedAttributeDefinitions(childXmlTypeName, pathName, parents, childFields, out); 488 parents.pop(); 489 } 490 } 491 } 492 493 private boolean shouldWriteDetails(MessageStructure ms) { 494 if (predefinedFieldMap.get(ms.getShortName().toLowerCase()) == null) { 495 return true; 496 } 497 if (ms.isOverriden()) { 498 return true; 499 } 500 // don't write out details for predefined fields that have not been overridden 501 return false; 502 } 503 504 private void writeGeneratedAttributeDefinition(String currentClassName, String parentName, Stack<String> parents, MessageStructure ms, XmlWriter out) { 505 506 //Create the abstract field 507 String pathName = calcPathName(parentName, ms); 508 String beanName = calcBeanName(pathName); 509 String baseKualiParentBean = this.calcBaseKualiParentBean(ms); 510 out.println(""); 511 out.indentPrintln("<bean id=\"" + beanName + "-generated\" abstract=\"true\" parent=\"" + baseKualiParentBean + "\">"); 512 out.incrementIndent(); 513 writeProperty("name", calcSimpleName(ms), out); 514 switch (this.calculateCategory(ms)) { 515 case PRIMITIVE: 516 if (this.shouldWriteDetails(ms)) { 517 writeProperty("shortLabel", calcShortLabel(ms), out); 518 writePropertyValue("summary", calcSummary(ms), out); 519 writeProperty("label", calcLabel(ms), out); 520 writePropertyValue("description", calcDescription(ms), out); 521 if (this.calcReadOnly(ms)) { 522 this.writeReadOnlyAttributeSecurity(out); 523 } 524 writeProperty("required", calcRequired(ms), out); 525 } 526 break; 527 case LIST_OF_PRIMITIVE: 528 // TODO: deal with once https://jira.kuali.org/browse/KULRICE-5439 is fixed 529 // for now treat the same as List of Complex, i.e. CollectionDefinition 530 writeProperty("shortLabel", calcShortLabel(ms), out); 531 writePropertyValue("summary", calcSummary(ms), out); 532 writeProperty("label", calcLabel(ms), out); 533 writeProperty("elementLabel", calcElementLabel(ms), out); 534 writePropertyValue("description", calcDescription(ms), out); 535 writeProperty("minOccurs", calcMinOccurs(ms), out); 536 writeProperty("dataObjectClass", calcDataObjectClass(ms), out); 537 break; 538 case LIST_OF_COMPLEX: 539 writeProperty("shortLabel", calcShortLabel(ms), out); 540 writePropertyValue("summary", calcSummary(ms), out); 541 writeProperty("label", calcLabel(ms), out); 542 writeProperty("elementLabel", calcElementLabel(ms), out); 543 writePropertyValue("description", calcDescription(ms), out); 544 writeProperty("minOccurs", calcMinOccurs(ms), out); 545 writeProperty("dataObjectClass", calcDataObjectClass(ms), out); 546 break; 547 case COMPLEX: 548 writeProperty("shortLabel", calcShortLabel(ms), out); 549 writePropertyValue("summary", calcSummary(ms), out); 550 writeProperty("label", calcLabel(ms), out); 551 writePropertyValue("description", calcDescription(ms), out); 552 writeProperty("required", calcRequired(ms), out); 553 writePropertyStart("dataObjectEntry", out); 554 out.indentPrintln("<bean parent=\"DataObjectEntry\">"); 555 out.incrementIndent(); 556 writeProperty("name", calcSimpleName(ms), out); 557 writeProperty("dataObjectClass", calcDataObjectClass(ms), out); 558 writeProperty("objectLabel", calcLabel(ms), out); 559 writePropertyValue("objectDescription", calcDescription(ms), out); 560 561 String childXmlTypeName = this.stripListOffEnd(ms.getType()); 562 List<MessageStructure> childFields = this.finder.findMessageStructures(childXmlTypeName); 563 writeAllGeneratedAttributeRefBeans(childXmlTypeName, pathName, parents, childFields, out); 564 out.indentPrintln("</bean>"); 565 writePropertyEnd(out); 566 break; 567 default: 568 throw new IllegalStateException("unknown/unhandled type " + ms.getId()); 569 } 570 out.decrementIndent(); 571 // TODO: implement maxoccurs 572 // if (isList(pd)) { 573 // addProperty("maxOccurs", "" + DictionaryConstants.UNBOUNDED, s); 574 // } 575 out.indentPrintln("</bean>"); 576 } 577 578 private String calcDataObjectClass(MessageStructure ms) { 579 XmlType msType = this.finder.findXmlType(this.stripListOffEnd(ms.getType())); 580 return this.calcDataObjectClass(msType); 581 } 582 583 private String calcBeanName(String pathName) { 584 return initUpper(className) + "." + pathName; 585 } 586 587 private String calcPathName(String parentName, MessageStructure ms) { 588 String name = this.calcSimpleName(ms); 589 if (parentName == null) { 590 return name; 591 } 592 return parentName + "." + name; 593 } 594 595 private String calcSimpleName(MessageStructure ms) { 596 String name = initLower(ms.getShortName()); 597 return name; 598 } 599 600 private boolean calcReadOnly(MessageStructure ms) { 601 if (ms.getReadOnly() == null) { 602 return false; 603 } 604 return true; 605 } 606 607 private void writeReadOnlyAttributeSecurity(XmlWriter out) { 608 out.indentPrintln("<!-- commented out until KRAD bug gets fixed that requires mask to also be entered"); 609 out.indentPrintln("<property name=\"attributeSecurity\">"); 610 out.indentPrintln("<ref bean=\"BaseKuali.readOnlyAttributeSecurity\"/>"); 611 out.indentPrintln("</property>"); 612 out.indentPrintln("-->"); 613 } 614 615 private String calcElementLabel(MessageStructure ms) { 616 String label = this.calcShortLabel(ms); 617 if (label.endsWith("s")) { 618 label = label.substring(0, label.length() - 1); 619 } 620 return label; 621 } 622 623 private String calcShortLabel(MessageStructure ms) { 624 return this.splitCamelCase(initUpper(ms.getShortName())); 625 } 626 627 private String calcLabel(MessageStructure ms) { 628 return ms.getName(); 629 } 630 631 private String calcSummary(MessageStructure ms) { 632 BreakIterator bi = BreakIterator.getSentenceInstance(); 633 String description = ms.getDescription(); 634 if (description == null) { 635 return "???"; 636 } 637 bi.setText(ms.getDescription()); 638 // one big sentence 639 if (bi.next() == BreakIterator.DONE) { 640 return ms.getDescription(); 641 } 642 String firstSentence = description.substring(0, bi.current()); 643 return firstSentence; 644 } 645 646 private String calcDescription(MessageStructure ms) { 647 return ms.getDescription(); 648 } 649 650 private String calcMinOccurs(MessageStructure ms) { 651 String required = this.calcRequired(ms); 652 if ("false".equals(required)) { 653 return "0"; 654 } 655 return "1"; 656 } 657 658 private String calcRequired(MessageStructure ms) { 659 if (ms.getRequired() == null) { 660 return "false"; 661 } 662 if (ms.getRequired().equalsIgnoreCase("Required")) { 663 return "true"; 664 } 665 // TODO: figure out what to do if it is qualified like "required on update" 666 return "false"; 667 } 668 669 private void writeManualObjectStructure(XmlWriter out) { 670 //Step 1, create the parent bean 671 out.println(""); 672 out.indentPrintln("<!-- " + className + "-->"); 673 //Create the actual instance of the bean 674 out.indentPrintln("<bean id=\"" + initUpper(className) + "\" parent=\"" + initUpper(className) + "-parent\"/>"); 675 out.indentPrintln("<bean id=\"" + initUpper(className) + "-parent\" abstract=\"true\" parent=\"" + initUpper(className) + "-generated\">"); 676 out.writeComment("insert any overrides to the generated object definitions here"); 677 out.indentPrintln("</bean>"); 678 679 //Step 2, loop through attributes 680 this.writeManualAttributeDefinitions(className, null, new Stack<String>(), this.messageStructures, out); 681 682 } 683 684 private void writeManualAttributeDefinitions(String currentClass, String parentName, 685 Stack<String> parents, List<MessageStructure> fields, XmlWriter out) { 686 if (parents.contains(currentClass)) { 687 return; 688 } 689 for (MessageStructure ms : fields) { 690 Category cat = this.calculateCategory(ms); 691 // skip dynamic attributes 692 switch (cat) { 693 case DYNAMIC_ATTRIBUTE: 694 continue; 695 } 696 697 String pathName = calcPathName(parentName, ms); 698 String beanName = calcBeanName(pathName); 699 String childXmlTypeName = this.stripListOffEnd(ms.getType()); 700 XmlType childXmlType = this.finder.findXmlType(childXmlTypeName); 701 if (childXmlType == null) { 702 throw new IllegalStateException(childXmlTypeName); 703 } 704 writeManualAttributeDefinition(currentClass, parentName, ms, out); 705 706 // Add complex sub-types fields 707 switch (cat) { 708 case COMPLEX: 709 parents.push(currentClass); 710 List<MessageStructure> childFields = this.finder.findMessageStructures(childXmlTypeName); 711 // if (childFields.isEmpty()) { 712 // throw new IllegalStateException(childXmlTypeName); 713 // } 714 writeManualAttributeDefinitions(childXmlTypeName, pathName, parents, childFields, out); 715 parents.pop(); 716 } 717 } 718 } 719 720 private void writeManualAttributeDefinition(String currentClass, String parentName, MessageStructure ms, XmlWriter out) { 721 722 //Create the abstract field 723 String pathName = calcPathName(parentName, ms); 724 String beanName = calcBeanName(pathName); 725 // String baseKualiType = this.calcBaseKualiType(ms); 726 //Create the actual bean instance 727 out.println(""); 728 out.indentPrintln("<bean id=\"" + beanName + "\" parent=\"" + beanName + "-parent\"/>"); 729 out.indentPrintln("<bean id=\"" + beanName + "-parent\" abstract=\"true\" parent=\"" + beanName + "-generated\">"); 730 out.writeComment("insert any overrides to the generated attribute definitions here"); 731 out.indentPrintln("</bean>"); 732 } 733 /** 734 * list of predefined fields that should map to entries in ks-base-dictionary.xml 735 */ 736 private static Map<String, String> predefinedFieldMap = null; 737 738 { 739 Map<String, String> map = new HashMap<String, String>(); 740 map.put("id", "BaseKuali.id"); 741 map.put("key", "BaseKuali.key"); 742 map.put("name", "BaseKuali.name"); 743 map.put("descr", "BaseKuali.descr"); 744 map.put("plain", "BaseKuali.descr.plain"); 745 map.put("formatted", "BaseKuali.descr.formatted"); 746 map.put("desc", "BaseKuali.desc"); // r1 compatibility 747 map.put("typeKey", "BaseKuali.typeKey"); 748 map.put("stateKey", "BaseKuali.stateKey"); 749 map.put("type", "BaseKuali.type"); // r1 compatibility 750 map.put("state", "BaseKuali.state"); // r1 compatibility 751 map.put("effectiveDate", "BaseKuali.effectiveDate"); 752 map.put("expirationDate", "BaseKuali.expirationDate"); 753 map.put("meta", "BaseKuali.meta"); 754 map.put("createTime", "BaseKuali.meta.createTime"); 755 map.put("updateTime", "BaseKuali.meta.updateTime"); 756 map.put("createId", "BaseKuali.meta.createId"); 757 map.put("updateId", "BaseKuali.meta.updateId"); 758 map.put("versionInd", "BaseKuali.meta.versionInd"); 759 // convert to lower case 760 predefinedFieldMap = new HashMap(map.size()); 761 for (String key : map.keySet()) { 762 predefinedFieldMap.put(key.toLowerCase(), map.get(key)); 763 } 764 } 765 /** 766 * list of fields that if they end with the key the should be based on the entry 767 * in ks-base-dictionary.xml 768 */ 769 private static Map<String, String> endsWithMap = null; 770 771 { 772 Map<String, String> map = new HashMap<String, String>(); 773 map.put("startDate", "BaseKuali.startDate"); 774 map.put("endDate", "BaseKuali.endDate"); 775 map.put("start", "BaseKuali.start"); 776 map.put("end", "BaseKuali.end"); 777 map.put("OrgId", "BaseKuali.orgId"); 778 map.put("OrgIds", "BaseKuali.orgId"); 779 map.put("PersonId", "BaseKuali.personId"); 780 map.put("PersonIds", "BaseKuali.personId"); 781 map.put("PrincipalId", "BaseKuali.principalId"); 782 map.put("PrincipalIds", "BaseKuali.principalId"); 783 map.put("CluId", "BaseKuali.cluId"); 784 map.put("CluIds", "BaseKuali.cluId"); 785 map.put("LuiId", "BaseKuali.luiId"); 786 map.put("LuiIds", "BaseKuali.luiId"); 787 map.put("AtpId", "BaseKuali.atpId"); 788 map.put("AtpIds", "BaseKuali.atpId"); 789 map.put("TermId", "BaseKuali.termId"); 790 map.put("TermIds", "BaseKuali.termId"); 791 map.put("HolidayCalendarId", "BaseKuali.holidayCalendarId"); 792 map.put("HolidayCalendarIds", "BaseKuali.holidayCalendarId"); 793 map.put("Code", "BaseKuali.code"); 794 // convert to lower case 795 endsWithMap = new HashMap(map.size()); 796 for (String key : map.keySet()) { 797 endsWithMap.put(key.toLowerCase(), map.get(key)); 798 } 799 } 800 /** 801 * list of types that if the type matches this key then 802 * it should be based on that type entry as defined in the 803 * ks-base-dictionary.xml 804 */ 805 private static Map<String, String> typeMap = null; 806 807 { 808 Map<String, String> map = new HashMap<String, String>(); 809 map.put("String", "BaseKuali.string"); 810 map.put("DateTime", "BaseKuali.dateTime"); 811 map.put("Date", "BaseKuali.date"); 812 map.put("Boolean", "BaseKuali.boolean"); 813 map.put("Integer", "BaseKuali.integer"); 814 map.put("Long", "BaseKuali.long"); 815 map.put("Float", "BaseKuali.float"); 816 map.put("Double", "BaseKuali.double"); 817 // convert to lower case 818 typeMap = new HashMap(map.size()); 819 for (String key : map.keySet()) { 820 typeMap.put(key.toLowerCase(), map.get(key)); 821 } 822 } 823 824 private String calcBaseKualiParentBean(MessageStructure ms) { 825 switch (this.calculateCategory(ms)) { 826 case COMPLEX: 827 return "ComplexAttributeDefinition"; 828 case LIST_OF_COMPLEX: 829 return "CollectionDefinition"; 830 case LIST_OF_PRIMITIVE: 831 // TODO: deal with once https://jira.kuali.org/browse/KULRICE-5439 is fixed 832 System.out.println("Treating list of primitives same as collection defintion: " + ms.getId()); 833 return "CollectionDefinition"; 834 case PRIMITIVE: 835 break; 836 default: 837 throw new IllegalStateException("unknown/uhandled category " + ms.getId()); 838 } 839 String name = ms.getShortName(); 840 String baseKualiType = predefinedFieldMap.get(name.toLowerCase()); 841 if (baseKualiType != null) { 842 return baseKualiType; 843 } 844 845 // check ends with 846 for (String key : endsWithMap.keySet()) { 847 if (name.toLowerCase().endsWith(key)) { 848 return endsWithMap.get(key); 849 } 850 } 851 852 // now key off the type 853 String type = this.stripListOffEnd(ms.getType()); 854 baseKualiType = typeMap.get(type.toLowerCase()); 855 if (baseKualiType != null) { 856 return baseKualiType; 857 } 858 throw new IllegalStateException("All primitives are supposed to be handled by a predefined type " + ms.getId()); 859 } 860 861 private String calcTitleAttribute() { 862 MessageStructure ms = null; 863 ms = this.findMessageStructure("name"); 864 if (ms != null) { 865 return initLower(ms.getShortName()); 866 } 867 ms = this.findMessageStructure("title"); 868 if (ms != null) { 869 return initLower(ms.getShortName()); 870 } 871 ms = this.findMessageStructureEndsWith("name"); 872 if (ms != null) { 873 return initLower(ms.getShortName()); 874 } 875 ms = this.findMessageStructureEndsWith("title"); 876 if (ms != null) { 877 return initLower(ms.getShortName()); 878 } 879 ms = this.findMessageStructure("key"); 880 if (ms != null) { 881 return initLower(ms.getShortName()); 882 } 883 // TODO: consider checking for ID and just returning null 884 System.out.println("XmlKradBaseDictionaryCreator: could not find a title attribute for " + this.className); 885 // ms = this.findMessageStructure("id"); 886 // if (ms != null) { 887 // return initLower(ms.getShortName()); 888 // } 889 return null; 890 } 891 892 private MessageStructure findMessageStructureEndsWith(String shortNameEndsWith) { 893 shortNameEndsWith = shortNameEndsWith.toLowerCase(); 894 for (MessageStructure ms : this.messageStructures) { 895 if (ms.getShortName().toLowerCase().endsWith(shortNameEndsWith)) { 896 return ms; 897 } 898 } 899 return null; 900 } 901 902 private MessageStructure findMessageStructure(String shortName) { 903 for (MessageStructure ms : this.messageStructures) { 904 if (ms.getShortName().equalsIgnoreCase(shortName)) { 905 return ms; 906 } 907 } 908 return null; 909 } 910 911 private MessageStructure getMessageStructure(String shortName) { 912 MessageStructure ms = this.findMessageStructure(shortName); 913 if (ms == null) { 914 throw new IllegalArgumentException(shortName); 915 } 916 return ms; 917 } 918 919 private List<String> calcPrimaryKeys() { 920 MessageStructure ms = null; 921 ms = this.findMessageStructure("id"); 922 if (ms != null) { 923 return Collections.singletonList(initLower(ms.getShortName())); 924 } 925 ms = this.findMessageStructure("key"); 926 if (ms != null) { 927 return Collections.singletonList(initLower(ms.getShortName())); 928 } 929 // just use the first field 930 if (this.messageStructures.size() > 0) { 931 ms = this.messageStructures.get(0); 932 return Collections.singletonList(ms.getShortName()); 933 } 934 return Collections.EMPTY_LIST; 935 } 936 937 private void writePropertyStart(String propertyName, XmlWriter out) { 938 out.indentPrintln("<property name=\"" + propertyName + "\">"); 939 out.incrementIndent(); 940 } 941 942 private void writePropertyEnd(XmlWriter out) { 943 out.decrementIndent(); 944 out.indentPrintln("</property>"); 945 } 946 947 private void writeProperty(String propertyName, String propertyValue, XmlWriter out) { 948 out.indentPrintln("<property name=\"" + propertyName + "\" value=\"" + replaceDoubleQuotes(propertyValue) + "\"/>"); 949 } 950 951 private void writePropertyValue(String propertyName, String propertyValue, XmlWriter out) { 952 writePropertyStart(propertyName, out); 953 out.indentPrintln("<value>"); 954 // TODO: worry about the value starting on a new line i.e. is it trimmed when loaded? 955 out.println(escapeHtml(propertyValue)); 956 out.indentPrintln("</value>"); 957 writePropertyEnd(out); 958 } 959 960 private String escapeHtml(String str) { 961 if (str == null) { 962 return null; 963 } 964 return StringEscapeUtils.escapeHtml(str); 965 } 966 967 private String replaceDoubleQuotes(String str) { 968 if (str == null) { 969 return null; 970 } 971 return str.replace("\"", "'"); 972 } 973 }