View Javadoc

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