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