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.ArrayList;
024    import java.util.Collections;
025    import java.util.Date;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Stack;
030    import org.apache.commons.lang.StringEscapeUtils;
031    
032    import org.kuali.student.contract.model.MessageStructure;
033    import org.kuali.student.contract.model.ServiceContractModel;
034    import org.kuali.student.contract.model.XmlType;
035    import org.kuali.student.contract.model.util.ModelFinder;
036    import org.kuali.student.contract.writer.XmlWriter;
037    
038    /**
039     *
040     * @author nwright
041     */
042    public class KradDictionaryCreator {
043    
044        private ServiceContractModel model;
045        private ModelFinder finder;
046        private String directory;
047        private String className;
048        private XmlType xmlType;
049        private XmlWriter gwriter;
050        private XmlWriter mwriter;
051        private List<MessageStructure> messageStructures;
052        private boolean writeManual;
053        private boolean writeGenerated;
054    
055        public KradDictionaryCreator(String directory,
056                ServiceContractModel model, String className, boolean writeManual, boolean writeGenerated) {
057            this.directory = directory;
058            this.model = model;
059            this.finder = new ModelFinder(this.model);
060            this.className = className;
061            this.xmlType = this.finder.findXmlType(className);
062            if (xmlType == null) {
063                throw new IllegalArgumentException(className);
064            }
065            this.messageStructures = this.finder.findMessageStructures(className);
066            this.writeManual = writeManual;
067            this.writeGenerated = writeGenerated;
068    //        if (this.messageStructures.isEmpty()) {
069    //            throw new IllegalStateException(className);
070    //        }
071        }
072    
073        public void write() {
074            this.initXmlWriters();
075            if (writeGenerated) {
076                this.writeSpringHeaderOpen(gwriter);
077                this.writeWarning(gwriter);
078                this.writeGeneratedImports(gwriter);
079                this.writeGeneratedObjectStructure(gwriter);
080                this.writeSpringHeaderClose(gwriter);
081            }
082            if (this.writeManual) {
083                this.writeSpringHeaderOpen(mwriter);
084                this.writeNote(mwriter);
085                this.writeManualImports(mwriter);
086                this.writeManualObjectStructure(mwriter);
087                this.writeSpringHeaderClose(mwriter);
088            }
089        }
090    
091        private void initXmlWriters() {
092            String generatedFileName = "/ks-" + initUpper(className) + "-dictionary-generated.xml";
093            String manualFileName = "/ks-" + initUpper(className) + "-dictionary.xml";
094    
095            File dir = new File(this.directory);
096            //System.out.indentPrintln ("Writing java class: " + fileName + " to " + dir.getAbsolutePath ());
097    
098            if (!dir.exists()) {
099                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    }