001: /*
002: * Copyright 2005-2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.opensource.org/licenses/ecl1.php
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.kuali.core.datadictionary;
018:
019: import java.io.Serializable;
020: import java.util.ArrayList;
021: import java.util.Collections;
022: import java.util.HashMap;
023: import java.util.HashSet;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import org.apache.commons.lang.StringUtils;
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031: import org.kuali.core.datadictionary.exception.DuplicateEntryException;
032: import org.kuali.core.exceptions.DuplicateKeyException;
033: import org.kuali.rice.KNSServiceLocator;
034:
035: /**
036: * Collection of named BusinessObjectEntry objects, each of which contains
037: * information relating to the display, validation, and general maintenance of a
038: * BusinessObject.
039: *
040: *
041: */
042: public class DataDictionary implements Serializable {
043: /**
044: *
045: */
046: private static final long serialVersionUID = 4707349557978305232L;
047:
048: // logger
049: private static final Log LOG = LogFactory
050: .getLog(DataDictionary.class);
051:
052: private DataDictionaryBuilder dataDictionaryBuilder;
053:
054: // keyed by BusinessObject class
055: private Map<String, BusinessObjectEntry> businessObjectEntries;
056:
057: // keyed by documentTypeName
058: private Map<String, DocumentEntry> documentEntries;
059:
060: // keyed by other things
061: private Map<Class, DocumentEntry> documentEntriesByDocumentClass;
062:
063: private Map<Class, DocumentEntry> documentEntriesByBusinessObjectClass;
064:
065: private Map<Class, DocumentEntry> documentEntriesByMaintainableClass;
066:
067: private Map<String, DataDictionaryEntry> entriesByJstlKey;
068:
069: private Set jstlKeys;
070:
071: private ValidationCompletionUtils validationCompletionUtils;
072:
073: private boolean allowOverrides = true;
074:
075: public DataDictionary(
076: ValidationCompletionUtils validationCompletionUtils,
077: DataDictionaryBuilder dataDictionaryBuilder) {
078: LOG.debug("creating new DataDictionary");
079:
080: this .validationCompletionUtils = validationCompletionUtils;
081:
082: // primary indices
083: businessObjectEntries = new HashMap<String, BusinessObjectEntry>();
084: documentEntries = new HashMap<String, DocumentEntry>();
085:
086: // alternate indices
087: documentEntriesByDocumentClass = new HashMap<Class, DocumentEntry>();
088: documentEntriesByBusinessObjectClass = new HashMap<Class, DocumentEntry>();
089: documentEntriesByMaintainableClass = new HashMap<Class, DocumentEntry>();
090: entriesByJstlKey = new HashMap<String, DataDictionaryEntry>();
091:
092: this .dataDictionaryBuilder = dataDictionaryBuilder;
093: jstlKeys = new HashSet();
094: }
095:
096: protected Map<String, String> getFileLocationMap() {
097: return dataDictionaryBuilder.getFileLocationMap();
098: }
099:
100: // called by digester
101: public void addDocumentEntry(DocumentEntry documentEntry) {
102: if (documentEntry == null) {
103: throw new IllegalArgumentException(
104: "invalid (null) documentEntry");
105: }
106: LOG.debug("calling addDocumentEntry '"
107: + documentEntry.getDocumentTypeName() + "'");
108:
109: String entryName = documentEntry.getDocumentTypeName();
110: if (!allowOverrides) {
111: if (documentEntries.containsKey(entryName)) {
112: throw new DuplicateEntryException(
113: "duplicate DocumentEntry for document type '"
114: + entryName + "'");
115: }
116: }
117: if ((documentEntry instanceof TransactionalDocumentEntry)
118: && (documentEntries.get(documentEntry
119: .getFullClassName()) != null)
120: && !((DocumentEntry) documentEntries.get(documentEntry
121: .getFullClassName())).getDocumentTypeName()
122: .equals(documentEntry.getDocumentTypeName())) {
123: throw new DataDictionaryException(
124: new StringBuffer(
125: "Two transactional document types may not share the same document class: this=")
126: .append(documentEntry.getDocumentTypeName())
127: .append(" / existing=").append(
128: ((DocumentEntry) documentEntries
129: .get(documentEntry
130: .getDocumentClass()
131: .getName()))
132: .getDocumentTypeName())
133: .toString());
134: }
135: if ((entriesByJstlKey.get(documentEntry.getJstlKey()) != null)
136: && !((DocumentEntry) documentEntries.get(documentEntry
137: .getJstlKey())).getDocumentTypeName().equals(
138: documentEntry.getDocumentTypeName())) {
139: throw new DataDictionaryException(
140: new StringBuffer(
141: "Two document types may not share the same jstl key: this=")
142: .append(documentEntry.getDocumentTypeName())
143: .append(" / existing=").append(
144: ((DocumentEntry) documentEntries
145: .get(documentEntry
146: .getJstlKey()))
147: .getDocumentTypeName())
148: .toString());
149: }
150:
151: documentEntry.completeValidation(validationCompletionUtils);
152:
153: documentEntries.put(entryName, documentEntry);
154: documentEntries.put(documentEntry.getFullClassName(),
155: documentEntry);
156: entriesByJstlKey.put(documentEntry.getJstlKey(), documentEntry);
157:
158: if (documentEntry instanceof TransactionalDocumentEntry) {
159: TransactionalDocumentEntry tde = (TransactionalDocumentEntry) documentEntry;
160:
161: documentEntriesByDocumentClass.put(tde.getDocumentClass(),
162: documentEntry);
163: documentEntries.put(tde.getDocumentClass().getSimpleName(),
164: documentEntry);
165: }
166: if (documentEntry instanceof MaintenanceDocumentEntry) {
167: MaintenanceDocumentEntry mde = (MaintenanceDocumentEntry) documentEntry;
168:
169: documentEntriesByBusinessObjectClass.put(mde
170: .getBusinessObjectClass(), documentEntry);
171: documentEntriesByMaintainableClass.put(mde
172: .getMaintainableClass(), documentEntry);
173: documentEntries.put(mde.getBusinessObjectClass()
174: .getSimpleName()
175: + "MaintenanceDocument", documentEntry);
176: }
177:
178: String jstlKey = documentEntry.getJstlKey();
179: if (!allowOverrides) {
180: if (jstlKeys.contains(jstlKey)) {
181: StringBuffer msg = new StringBuffer(
182: "unable to add jstlKey for documentType");
183: msg.append(StringUtils.substringAfterLast(documentEntry
184: .getDocumentTypeName(), "."));
185: msg.append(": key '");
186: msg.append(jstlKey);
187: msg.append("' is already in use");
188:
189: throw new DuplicateKeyException(msg.toString());
190: }
191: }
192: jstlKeys.add(jstlKey);
193: documentEntry.validateAuthorizations(KNSServiceLocator
194: .getKualiGroupService());
195: documentEntry.validateAuthorizer(KNSServiceLocator
196: .getKualiConfigurationService(),
197: validationCompletionUtils);
198: documentEntry.expandAttributeReferences(this ,
199: validationCompletionUtils);
200: KNSServiceLocator.getAuthorizationService()
201: .setupAuthorizations(documentEntry);
202: }
203:
204: public void addBusinessObjectEntry(
205: BusinessObjectEntry businessObjectEntry) {
206: if (businessObjectEntry == null) {
207: throw new IllegalArgumentException(
208: "invalid (null) businessObjectEntry");
209: }
210: LOG.debug("calling addBusinessObjectEntry '"
211: + businessObjectEntry.getBusinessObjectClass()
212: .getName() + "'");
213:
214: String entryName = businessObjectEntry.getBusinessObjectClass()
215: .getName();
216: if (!allowOverrides) {
217: if (businessObjectEntries.containsKey(entryName)) {
218: throw new DuplicateEntryException(
219: "duplicate BusinessObjectEntry for class '"
220: + entryName + "'");
221: }
222: }
223: if ((businessObjectEntries
224: .get(businessObjectEntry.getJstlKey()) != null)
225: && !((BusinessObjectEntry) businessObjectEntries
226: .get(businessObjectEntry.getJstlKey()))
227: .getBusinessObjectClass().equals(
228: businessObjectEntry
229: .getBusinessObjectClass())) {
230: throw new DataDictionaryException(
231: new StringBuffer(
232: "Two business object classes may not share the same jstl key: this=")
233: .append(
234: businessObjectEntry
235: .getBusinessObjectClass())
236: .append(" / existing=")
237: .append(
238: ((BusinessObjectEntry) businessObjectEntries
239: .get(businessObjectEntry
240: .getJstlKey()))
241: .getBusinessObjectClass())
242: .toString());
243: }
244:
245: businessObjectEntry
246: .completeValidation(validationCompletionUtils);
247:
248: businessObjectEntries.put(entryName, businessObjectEntry);
249: businessObjectEntries.put(businessObjectEntry
250: .getBusinessObjectClass().getSimpleName(),
251: businessObjectEntry);
252: entriesByJstlKey.put(businessObjectEntry.getJstlKey(),
253: businessObjectEntry);
254:
255: String jstlKey = businessObjectEntry.getJstlKey();
256: if (!allowOverrides) {
257: if (jstlKeys.contains(jstlKey)) {
258: StringBuffer msg = new StringBuffer(
259: "unable to add jstlKey for businessObject");
260: msg.append(StringUtils.substringAfterLast(
261: businessObjectEntry.getClass().getName(), "."));
262: msg.append(": key '");
263: msg.append(jstlKey);
264: msg.append("' is already in use");
265:
266: throw new DuplicateKeyException(msg.toString());
267: }
268: }
269: jstlKeys.add(jstlKey);
270: businessObjectEntry.expandAttributeReferences(this ,
271: validationCompletionUtils);
272: }
273:
274: /**
275: * This method provides the Map of all "components" (i.e. lookup, inquiry, and document titles), keyed by business object or document class names
276: *
277: * @return map of component names, keyed by class name
278: */
279: protected Map<String, Set<String>> getComponentNamesByClassName() {
280: Map<String, Set<String>> componentNamesByClassName = new HashMap<String, Set<String>>();
281: for (String businessObjectEntryKey : businessObjectEntries
282: .keySet()) {
283: BusinessObjectEntry businessObjectEntry = businessObjectEntries
284: .get(businessObjectEntryKey);
285: Set<String> componentNames = new HashSet<String>();
286: if (businessObjectEntry.getLookupDefinition() != null) {
287: componentNames.add(businessObjectEntry
288: .getLookupDefinition().getTitle());
289: }
290: if (businessObjectEntry.getInquiryDefinition() != null) {
291: componentNames.add(businessObjectEntry
292: .getInquiryDefinition().getTitle());
293: }
294: componentNamesByClassName.put(businessObjectEntry
295: .getFullClassName(), componentNames);
296: }
297: for (Class businessObjectClass : documentEntriesByBusinessObjectClass
298: .keySet()) {
299: DocumentEntry documentEntry = documentEntriesByBusinessObjectClass
300: .get(businessObjectClass);
301: if (componentNamesByClassName
302: .containsKey(businessObjectClass.getName())) {
303: componentNamesByClassName.get(
304: businessObjectClass.getName()).add(
305: documentEntry.getLabel());
306: } else {
307: Set<String> componentNames = new HashSet<String>();
308: componentNames.add(documentEntry.getLabel());
309: componentNamesByClassName.put(businessObjectClass
310: .getName(), componentNames);
311: }
312: }
313: for (Class documentClass : documentEntriesByDocumentClass
314: .keySet()) {
315: DocumentEntry documentEntry = documentEntriesByDocumentClass
316: .get(documentClass);
317: Set<String> componentNames = new HashSet<String>();
318: componentNames.add(documentEntry.getLabel());
319: componentNamesByClassName.put(documentClass.getName(),
320: componentNames);
321: }
322: return componentNamesByClassName;
323: }
324:
325: /**
326: * @param className
327: * @return BusinessObjectEntry for the named class, or null if none exists
328: */
329: public BusinessObjectEntry getBusinessObjectEntry(String className) {
330: return getBusinessObjectEntry(className, true);
331: }
332:
333: /**
334: * @param className
335: * @return BusinessObjectEntry for the named class, or null if none exists
336: */
337: public BusinessObjectEntry getBusinessObjectEntry(String className,
338: boolean parseOnFail) {
339: if (StringUtils.isBlank(className)) {
340: throw new IllegalArgumentException(
341: "invalid (blank) className");
342: }
343: LOG.debug("calling getBusinessObjectEntry '" + className + "'");
344: int index = className.indexOf("$$");
345: if (index >= 0) {
346: className = className.substring(0, index);
347: }
348: // LOG.info("calling getBusinessObjectEntry truncated '" + className + "'");
349:
350: BusinessObjectEntry boe = businessObjectEntries.get(className);
351: if (boe == null && parseOnFail) {
352: LOG.debug("Unable to find BusinessObjectEntry '"
353: + className + "' -- calling parseBO()");
354: this .dataDictionaryBuilder.parseBO(className,
355: isAllowOverrides());
356: }
357: return businessObjectEntries.get(className);
358: }
359:
360: /**
361: * @return List of businessObject classnames
362: */
363: public List<String> getBusinessObjectClassNames() {
364: List classNames = new ArrayList();
365: classNames.addAll(this .businessObjectEntries.keySet());
366:
367: return Collections.unmodifiableList(classNames);
368: }
369:
370: /**
371: * @return Map of (classname, BusinessObjectEntry) pairs
372: */
373: public Map<String, BusinessObjectEntry> getBusinessObjectEntries() {
374: return Collections.unmodifiableMap(this .businessObjectEntries);
375: }
376:
377: /**
378: * @param className
379: * @return DataDictionaryEntryBase for the named class, or null if none
380: * exists
381: */
382: public DataDictionaryEntry getDictionaryObjectEntry(String className) {
383: if (StringUtils.isBlank(className)) {
384: throw new IllegalArgumentException(
385: "invalid (blank) className");
386: }
387: LOG.debug("calling getDictionaryObjectEntry '" + className
388: + "'");
389: int index = className.indexOf("$$");
390: if (index >= 0) {
391: className = className.substring(0, index);
392: }
393:
394: // look in the JSTL key cache
395: DataDictionaryEntry entry = entriesByJstlKey.get(className);
396: if (entry == null) {
397: // look in the BO cache
398: entry = getBusinessObjectEntry(className, false);
399: if (entry == null) {
400: //look in the document cache
401: entry = getDocumentEntry(className, false);
402:
403: // the object does not exist in the DD currently, attempt to parse the file
404: if (entry == null) {
405: this .dataDictionaryBuilder.parseDocument(className,
406: isAllowOverrides());
407: // re-try the BO and document caches after the parse
408: entry = getDocumentEntry(className, false);
409: if (entry == null) {
410: entry = getBusinessObjectEntry(className, true);
411: }
412: }
413: }
414: }
415:
416: return entry;
417: }
418:
419: public DocumentEntry getDocumentEntry(String documentTypeDDKey) {
420: return getDocumentEntry(documentTypeDDKey, true);
421: }
422:
423: public DocumentEntry getDocumentEntry(String documentTypeDDKey,
424: boolean parseOnFail) {
425: if (StringUtils.isBlank(documentTypeDDKey)) {
426: throw new IllegalArgumentException(
427: "invalid (blank) documentTypeName");
428: }
429: LOG.debug("calling getDocumentEntry by documentTypeName '"
430: + documentTypeDDKey + "'");
431:
432: DocumentEntry de = documentEntries.get(documentTypeDDKey);
433: if (de == null) {
434: // need to attempt to convert the key to a class since the documentEntries...
435: // maps are keyed by class objects rather than class names
436: try {
437: Class clazz = Class.forName(documentTypeDDKey);
438: de = documentEntriesByBusinessObjectClass.get(clazz);
439: if (de == null) {
440: de = documentEntriesByDocumentClass.get(clazz);
441: }
442: } catch (ClassNotFoundException ex) {
443: // do nothing, just skip if not a valid class name
444: }
445: }
446: if (de == null && parseOnFail) {
447: this .dataDictionaryBuilder.parseDocument(documentTypeDDKey,
448: isAllowOverrides());
449: de = documentEntries.get(documentTypeDDKey);
450: }
451: return de;
452: }
453:
454: /**
455: * Note: only MaintenanceDocuments are indexed by businessObject Class
456: *
457: * This is a special case that is referenced in one location. Do we need
458: * another map for this stuff??
459: *
460: * @param businessObjectClass
461: * @return DocumentEntry associated with the given Class, or null if there
462: * is none
463: */
464: public MaintenanceDocumentEntry getMaintenanceDocumentEntryForBusinessObjectClass(
465: Class businessObjectClass) {
466: if (businessObjectClass == null) {
467: throw new IllegalArgumentException(
468: "invalid (null) businessObjectClass");
469: }
470: LOG.debug("calling getDocumentEntry by businessObjectClass '"
471: + businessObjectClass + "'");
472:
473: MaintenanceDocumentEntry mde = (MaintenanceDocumentEntry) documentEntriesByBusinessObjectClass
474: .get(businessObjectClass);
475: if (mde == null) {
476: //Before attempting to parse the DD again, try to look in the documentEntries, if we found
477: //it there, then we'll return null for this method because it means that the businessObjectClass
478: //is not in maintenance document, it's transactional.
479: if (documentEntries.get(businessObjectClass.getName()) != null) {
480: return null;
481: }
482: this .dataDictionaryBuilder.parseMaintenanceDocument(
483: businessObjectClass.getName(), true);
484: }
485: return (MaintenanceDocumentEntry) documentEntriesByBusinessObjectClass
486: .get(businessObjectClass);
487: }
488:
489: public Map<String, DocumentEntry> getDocumentEntries() {
490: return Collections.unmodifiableMap(this .documentEntries);
491: }
492:
493: public void setAllowOverrides(boolean allowOverrides) {
494: LOG.debug("calling setAllowOverrides " + allowOverrides);
495:
496: this .allowOverrides = allowOverrides;
497: }
498:
499: public boolean isAllowOverrides() {
500: return allowOverrides;
501: }
502:
503: private boolean completelyLoaded = false;
504:
505: public void forceCompleteDataDictionaryLoad() {
506: if (!completelyLoaded) {
507: for (String key : getFileLocationMap().keySet()) {
508: getDictionaryObjectEntry(key);
509: }
510: completelyLoaded = true;
511: }
512: }
513:
514: }
|