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