View Javadoc

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