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