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