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