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