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