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