View Javadoc
1   /**
2    * Copyright 2004-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.student.datadictionary.util;
17  
18  import java.io.File;
19  import java.io.FileNotFoundException;
20  import java.io.FileOutputStream;
21  import java.io.PrintStream;
22  import java.text.BreakIterator;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.Stack;
30  
31  import org.apache.commons.lang.StringEscapeUtils;
32  import org.kuali.student.contract.model.MessageStructure;
33  import org.kuali.student.contract.model.ServiceContractModel;
34  import org.kuali.student.contract.model.XmlType;
35  import org.kuali.student.contract.model.util.ModelFinder;
36  import org.kuali.student.contract.writer.XmlWriter;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * 
42   * @author nwright
43   */
44  public class KradDictionaryCreator {
45  
46  	private static final Logger log = LoggerFactory
47  			.getLogger(KradDictionaryCreator.class);
48  
49  	private ServiceContractModel model;
50  	private ModelFinder finder;
51  	private String directory;
52  	private String className;
53  	private XmlType xmlType;
54  	private XmlWriter gwriter;
55  	private XmlWriter mwriter;
56  	private List<MessageStructure> messageStructures;
57  	private boolean writeManual;
58  	private boolean writeGenerated;
59  	private String generatedFilePath;
60  	private String manualFilePath;
61  
62  	private boolean initialized = false;
63  
64  	private List<String> enumeratedTypeList;
65  
66  
67  	public KradDictionaryCreator(String directory, ServiceContractModel model,
68  			String className, boolean writeManual, boolean writeGenerated, Map<String, String> typeOverrides) {
69  		this.directory = directory;
70  		this.model = model;
71  		this.finder = new ModelFinder(this.model);
72  		this.className = className;
73  		this.xmlType = this.finder.findXmlType(className);
74  		if (xmlType == null) {
75  			throw new IllegalArgumentException(className);
76  		}
77  		this.messageStructures = this.finder.findMessageStructures(className);
78  		this.writeManual = writeManual;
79  		this.writeGenerated = writeGenerated;
80  		// if (this.messageStructures.isEmpty()) {
81  		// throw new IllegalStateException(className);
82  		// }
83  		
84  		initializeTypes (typeOverrides);
85  	}
86  
87  	private void initializeTypes(Map<String, String> typeOverrides) {
88  		
89  		if (KradDictionaryCreator.predefinedFieldMap == null) {
90  			Map<String, String> map = new HashMap<String, String>();
91  			map.put("id", "BaseKuali.id");
92  			map.put("key", "BaseKuali.key");
93  			map.put("name", "BaseKuali.name");
94  			map.put("descr", "BaseKuali.descr");
95  			map.put("plain", "BaseKuali.descr.plain");
96  			map.put("formatted", "BaseKuali.descr.formatted");
97  			map.put("desc", "BaseKuali.desc"); // r1 compatibility
98  			map.put("typeKey", "BaseKuali.typeKey");
99  			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 }