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