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