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