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.File;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.net.URL;
023: import java.util.HashMap;
024: import java.util.Map;
025:
026: import org.apache.commons.digester.Digester;
027: import org.apache.commons.digester.Rules;
028: import org.apache.commons.digester.xmlrules.DigesterLoader;
029: import org.apache.commons.lang.StringUtils;
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.kuali.core.datadictionary.exception.DuplicateEntryException;
033: import org.kuali.core.datadictionary.exception.InitException;
034: import org.kuali.core.datadictionary.exception.ParseException;
035: import org.kuali.core.datadictionary.exception.SourceException;
036: import org.kuali.core.service.KualiConfigurationService;
037: import org.kuali.core.service.KualiGroupService;
038: import org.springframework.core.io.DefaultResourceLoader;
039: import org.springframework.core.io.Resource;
040: import org.xml.sax.Locator;
041: import org.xml.sax.SAXException;
042:
043: import edu.iu.uis.eden.util.ClassLoaderUtils;
044:
045: /**
046: * Assembles a DataDictionary from the contents of one or more specifed XML
047: * files or directories containing XML files.
048: *
049: *
050: */
051: public class DataDictionaryBuilder {
052: // logger
053: private static Log LOG = LogFactory
054: .getLog(DataDictionaryBuilder.class);
055:
056: protected static final String PACKAGE_PREFIX = "/org/kuali/core/datadictionary/";
057:
058: // DTD registration info
059: protected final static String[][] DTD_REGISTRATION_INFO = { {
060: "-//Kuali Project//DTD Data Dictionary 1.0//EN",
061: PACKAGE_PREFIX + "dataDictionary-1_0.dtd" }, };
062:
063: private KualiConfigurationService kualiConfigurationService;
064:
065: private KualiGroupService kualiGroupService;
066:
067: protected DataDictionary dataDictionary;
068:
069: private boolean exceptionsOccurred;
070:
071: private static Map<String, String> fileLocationMap = new HashMap<String, String>();
072:
073: /**
074: * Default constructor
075: */
076: public DataDictionaryBuilder(
077: ValidationCompletionUtils validationCompletionUtils) {
078: LOG.info("created DataDictionaryBuilder");
079:
080: dataDictionary = new DataDictionary(validationCompletionUtils,
081: this );
082: setExceptionsOccurred(false);
083: }
084:
085: /**
086: * @return the current dataDictionary
087: * @throws IllegalStateException
088: * if any exceptions occurred during calls to add*Entries
089: * constructing the dataDictionary
090: */
091: public DataDictionary getDataDictionary() {
092: if (getExceptionsOccurred()) {
093: throw new IllegalStateException(
094: "illegal attempt to retrieve invalid DataDictionary");
095: }
096:
097: return this .dataDictionary;
098: }
099:
100: protected Map<String, String> getFileLocationMap() {
101: return fileLocationMap;
102: }
103:
104: /**
105: * Given the name of an XML file, or of a directory containing XML files,
106: * adds the entries defined in the XML file or files to the DataDictionary
107: * being built. Duplicate class entries (antries using a classname already
108: * in use) will result in a DuplicateEntryException being added to the
109: * cumulative entryExceptions list. If sourceMustExist is true, a
110: * nonexistent source will result in a SourceException being thrown.
111: *
112: * @param sourceName
113: * XML file or directory containing XML files to be added
114: * @param sourceMustExist
115: * throw a SourceException if the given source cannot be found
116: * @throws IllegalArgumentException
117: * if the given sourceName is blank
118: * @throws DuplicateEntryException
119: * if one of the files being added contains an entry using a
120: * classname for which an entrry has already been defined
121: * @throws ParseException
122: * if an error occurs processing an entry file
123: * @throws SourceException
124: * if sourceMustExist is true and the source can't be found
125: */
126: public void addUniqueEntries(String sourceName,
127: boolean sourceMustExist) {
128: // addEntriesWrapper(sourceName, sourceMustExist, false);
129: try {
130: indexSource(sourceName, sourceMustExist);
131: } catch (IOException ioe) {
132: throw new DataDictionaryException("Error indexing "
133: + sourceName, ioe);
134: }
135: }
136:
137: /**
138: * Given the name of an XML file, or of a directory containing XML files,
139: * adds the entries defined in the XML file or files to the DataDictionary
140: * being built. Duplicate class entries will override earlier class entries.
141: *
142: * @param sourceName
143: * XML file or directory containing XML files to be added
144: * @param sourceMustExist
145: * throw a SourceException if the given source cannot be found
146: * @throws IllegalArgumentException
147: * if the given sourceName is blank
148: * @throws ParseException
149: * if an error occurs processing an entry file
150: * @throws SourceException
151: * if sourceMustExist is true and the source can't be found
152: */
153: public void addOverrideEntries(String sourceName,
154: boolean sourceMustExist) {
155: // addEntriesWrapper(sourceName, sourceMustExist, true);
156: try {
157: indexSource(sourceName, sourceMustExist);
158: } catch (IOException ioe) {
159: throw new DataDictionaryException("Error indexing "
160: + sourceName, ioe);
161: }
162: }
163:
164: protected void indexSource(String sourceName,
165: boolean sourceMustExist) throws IOException {
166:
167: if (sourceName == null) {
168: throw new DataDictionaryException(
169: "Source Name given is null");
170: }
171:
172: if (sourceName.indexOf(".xml") < 0) {
173: Resource resource = getFileResource(sourceName);
174: if (resource.exists()) {
175: indexSource(resource.getFile());
176: } else {
177: if (sourceMustExist) {
178: throw new DataDictionaryException("DD Resource "
179: + sourceName + " not found");
180: }
181: LOG.debug("Could not find " + sourceName);
182: }
183: } else {
184: LOG.debug("adding sourceName " + sourceName + " ");
185: if (sourceMustExist) {
186: Resource resource = getFileResource(sourceName);
187: if (!resource.exists()) {
188: throw new DataDictionaryException("DD Resource "
189: + sourceName + " not found");
190: }
191: }
192: String indexName = sourceName.substring(sourceName
193: .lastIndexOf("/") + 1, sourceName.indexOf(".xml"));
194: fileLocationMap.put(indexName, sourceName);
195: }
196: }
197:
198: private Resource getFileResource(String sourceName) {
199: DefaultResourceLoader resourceLoader = new DefaultResourceLoader(
200: ClassLoaderUtils.getDefaultClassLoader());
201: return resourceLoader.getResource(sourceName);
202: }
203:
204: protected void indexSource(File dir) {
205: for (File file : dir.listFiles()) {
206: if (file.isDirectory()) {
207: indexSource(file);
208: } else if (file.getName().indexOf(".xml") > 0) {
209: String indexName = file.getName().substring(
210: file.getName().lastIndexOf("/") + 1,
211: file.getName().indexOf(".xml"));
212: fileLocationMap.put(indexName, "file:"
213: + file.getAbsolutePath());
214: } else {
215: LOG.debug("Skipping non xml file "
216: + file.getAbsolutePath() + " in DD load");
217: }
218: }
219: }
220:
221: public void parseBO(String boName, boolean allowsOverrides) {
222: String boKey = boName.substring(boName.lastIndexOf(".") + 1);
223: String boFileName = fileLocationMap.get(boKey);
224: if (boFileName == null) {
225: return;
226: }
227: addEntriesWrapper(boFileName, true, allowsOverrides);
228: }
229:
230: public void parseDocument(String documentTypeDDKey,
231: boolean allowsOverrides) {
232: // String className = documentTypeClass.getName();
233: // String documentKey = documentTypeName;
234:
235: String documentFileName = fileLocationMap
236: .get(documentTypeDDKey);
237:
238: if (documentFileName == null) {
239: documentFileName = fileLocationMap.get(documentTypeDDKey
240: + "MaintenanceDocument");
241: if (documentFileName == null) {
242: if (documentTypeDDKey.indexOf(".") > -1) {
243: documentTypeDDKey = documentTypeDDKey
244: .substring(documentTypeDDKey
245: .lastIndexOf(".") + 1);
246: }
247: documentFileName = fileLocationMap
248: .get(documentTypeDDKey + "MaintenanceDocument");
249: // handle the global documents, whose business object ends with the work document, but where the DD file name does not contain the suffix
250: // e.g., BO: AccountChangeDocument, BO Maint Doc XML: AccountChangeMaintenanceDocument
251: if (documentFileName == null
252: && documentTypeDDKey.endsWith("Document")) {
253: // reduce the key to the BO name (strip Kuali and Document off the ends)
254: documentTypeDDKey = documentTypeDDKey.replace(
255: "Document", "").replace("Kuali", "");
256: documentFileName = fileLocationMap
257: .get(documentTypeDDKey
258: + "MaintenanceDocument");
259: }
260: if (documentFileName == null) {
261: documentFileName = fileLocationMap
262: .get(documentTypeDDKey + "Document");
263: if (documentFileName == null) {
264: documentFileName = fileLocationMap.get("Kuali"
265: + documentTypeDDKey + "Document");
266: if (documentFileName == null) {
267: if (documentTypeDDKey.indexOf("Document") > -1) {
268: documentFileName = fileLocationMap
269: .get("Kuali"
270: + documentTypeDDKey);
271: }
272: if (documentFileName == null) {
273: documentFileName = fileLocationMap
274: .get("Kuali"
275: + documentTypeDDKey
276: + "MaintenanceDocument");
277: if (documentFileName == null) {
278: return;
279: }
280: }
281: }
282: }
283: }
284: }
285: }
286: addEntriesWrapper(documentFileName, true, allowsOverrides);
287: }
288:
289: public void parseMaintenanceDocument(String businessObjectDDKey,
290: boolean allowsOverrides) {
291: if (businessObjectDDKey.indexOf(".") > -1) {
292: businessObjectDDKey = StringUtils.substringAfterLast(
293: businessObjectDDKey, ".");
294: }
295:
296: String documentFileName = fileLocationMap
297: .get(businessObjectDDKey + "MaintenanceDocument");
298: if (documentFileName == null) {
299: documentFileName = fileLocationMap.get("Kuali"
300: + businessObjectDDKey + "MaintenanceDocument");
301: if (documentFileName == null) {
302: return;
303: }
304: }
305: addEntriesWrapper(documentFileName, true, allowsOverrides);
306: }
307:
308: /**
309: * Wraps addEntries with a try-catch block which prevents SourceExceptions
310: * from escaping if sourceMustExist is false.
311: *
312: * @throws SourceException
313: * if the given source does not exist, and sourceMustExist is
314: * true
315: */
316: protected void addEntriesWrapper(String sourceName,
317: boolean sourceMustExist, boolean allowOverrides) {
318: LOG.info("adding dataDictionary entries from source '"
319: + sourceName + "'");
320: try {
321: addEntries(sourceName, allowOverrides);
322: } catch (SourceException e) {
323: if (sourceMustExist) {
324: throw e;
325: }
326: }
327: LOG.debug("added dataDictionary entries from source '"
328: + sourceName + "'");
329: }
330:
331: private ThreadLocal<Rules> digesterRules = new ThreadLocal<Rules>();
332:
333: /**
334: * Parses each XML file on the given list, adding the class entry or entries
335: * defined there to the current DataDictionary. If allowOverrides is true,
336: * treats duplicate entries (second and subsequent entry using a given
337: * classname) as an error; otherwise, the last entry processed using a given
338: * classname will replace earlier entries.
339: *
340: * @param sourceName
341: * name of XML file or directory containing XML files
342: * @param sourceMustExist
343: * throw a SourceException if the given source cannot be found
344: * @param allowOverrides
345: * @throws ParseException
346: * if an error occurs when processing the given list of xmlFiles
347: * @throws DuplicateEntryException
348: * if allowOverrides is false, and an entry is defined using a
349: * classname for which an entry already exists
350: */
351: protected synchronized void addEntries(String sourceName,
352: boolean allowOverrides) {
353: if (StringUtils.isEmpty(sourceName)) {
354: throw new IllegalArgumentException(
355: "invalid (empty) sourceName");
356: }
357:
358: // ensure a separate copy of the digester rules per accessing thread
359: if (digesterRules.get() == null) {
360: LOG.debug("addEntries(): Loading Digester Rules");
361: digesterRules.set(loadRules());
362: }
363: Digester digester = buildDigester(digesterRules.get());
364:
365: dataDictionary.setAllowOverrides(allowOverrides);
366:
367: try {
368: DefaultResourceLoader resourceLoader = new DefaultResourceLoader(
369: ClassLoaderUtils.getDefaultClassLoader());
370: if (sourceName.indexOf("classpath:") > -1) {
371: digest(resourceLoader.getResource(sourceName)
372: .getInputStream(), sourceName, digester);
373: } else {
374: digest(
375: resourceLoader.getResource(sourceName)
376: .getFile(), digester);
377: }
378: } catch (DataDictionaryException dde) {
379: LOG
380: .error("Rethrowing DataDictionaryException encountered while parsing sourceName: "
381: + sourceName);
382: throw dde;
383: } catch (Exception e) {
384: throw new DataDictionaryException(
385: "Problems parsing DD for sourceName: " + sourceName,
386: e);
387: } finally {
388: if (digester != null) {
389: digester.clear();
390: }
391: }
392:
393: clearCurrentDigester();
394: clearCurrentFilename();
395: }
396:
397: // /**
398: // * @param source
399: // * XML file, or package containing XML files (which, if a
400: // * package, must end with the ".xml" extension)
401: // * @return List of XML Files located using the given sourceName
402: // * @throws SAXException
403: // * @throws IOException
404: // * @throws IOException
405: // * @throws IOException
406: // * if there's a problem locating the named source
407: // */
408: // protected void listSourceFiles(String sourceName, Digester digester)
409: // throws Exception {
410: // DefaultResourceLoader resourceLoader = new
411: // DefaultResourceLoader(ClassLoaderUtils.getDefaultClassLoader());
412: // Resource resource = resourceLoader.getResource(sourceName);
413: //
414: // if (sourceName.indexOf(".xml") < 0) {
415: // if (resource.exists()) {
416: // listSources(resource.getFile(), digester);
417: // } else {
418: // LOG.debug("Could not find " + sourceName);
419: // }
420: // } else {
421: // InputStream is = resource.getInputStream();
422: // if (is == null) {
423: // throw new DataDictionaryException("Cannot find file: " + sourceName);
424: // }
425: // LOG.debug("Adding file " + resource.getFilename() + " to DD.");
426: // digest(is, sourceName, digester);
427: // }
428: // }
429: //
430: // protected void listSources(File sourceDir, Digester digester) throws
431: // Exception {
432: // for (File file : sourceDir.listFiles()) {
433: // if (file.isDirectory()) {
434: // listSources(file, digester);
435: // } else if (file.getName().indexOf(".xml") > 0) {
436: // digest(file, digester);
437: // } else {
438: // LOG.info("Skipping non xml file " + file.getAbsolutePath() + " in DD
439: // load");
440: // }
441: // }
442: // }
443:
444: protected void setupDigester(Digester digester) {
445: setCurrentDigester(digester);
446: digester.push(dataDictionary);
447:
448: }
449:
450: protected void digest(InputStream inputStream, String fileName,
451: Digester digester) throws Exception {
452: setupDigester(digester);
453: setCurrentFilename(fileName);
454: digester.setErrorHandler(new XmlErrorHandler(fileName));
455: digester.parse(inputStream);
456: }
457:
458: protected void digest(File file, Digester digester)
459: throws IOException, SAXException {
460: setupDigester(digester);
461: digester.setErrorHandler(new XmlErrorHandler(file.getName()));
462: setCurrentFilename(file.getName());
463: digester.parse(file);
464: }
465:
466: /**
467: * @return Rules loaded from the appropriate XML file
468: */
469: protected Rules loadRules() {
470: // locate Digester rules
471: URL rulesUrl = DataDictionaryBuilder.class
472: .getResource(PACKAGE_PREFIX + "digesterRules.xml");
473: if (rulesUrl == null) {
474: throw new InitException(
475: "unable to locate digester rules file");
476: }
477:
478: // create and init digester
479: Digester digester = DigesterLoader.createDigester(rulesUrl);
480:
481: return digester.getRules();
482: }
483:
484: /**
485: * @return fully-initialized Digester used to process entry XML files
486: */
487: protected Digester buildDigester(Rules rules) {
488: Digester digester = new Digester();
489: digester.setNamespaceAware(false);
490: digester.setValidating(true);
491:
492: // register DTD(s)
493: for (int i = 0; i < DTD_REGISTRATION_INFO.length; ++i) {
494: String dtdPublic = DTD_REGISTRATION_INFO[i][0];
495: String dtdPath = DTD_REGISTRATION_INFO[i][1];
496:
497: URL dtdUrl = DataDictionaryBuilder.class
498: .getResource(dtdPath);
499: if (dtdUrl == null) {
500: throw new InitException("unable to locate DTD at \""
501: + dtdPath + "\"");
502: }
503: digester.register(dtdPublic, dtdUrl.toString());
504: }
505:
506: digester.setRules(rules);
507:
508: return digester;
509: }
510:
511: /**
512: * This is a rather ugly hack which expose the Digester being used to parse
513: * a given XML file so that error messages generated during parsing can
514: * contain file and line number info.
515: * <p>
516: * If we weren't using an XML file to configure Digester, I'd do this by
517: * rewriting all of the rules so that they accepted the Digester instance as
518: * a param, which would be considerably less ugly.
519: */
520:
521: /**
522: * @return name of the XML file currently being parsed
523: * @throws IllegalStateException
524: * if parsing is not in progress
525: */
526: public static String getCurrentFileName() {
527: // try to prevent invalid access to nonexistent filename
528: if (currentFilename == null) {
529: throw new IllegalStateException("current filename is null");
530: }
531:
532: return currentFilename;
533: }
534:
535: /**
536: * @return line number in the XML file currently being parsed
537: * @throws IllegalStateException
538: * if parsing is not in progress
539: */
540: public static int getCurrentLineNumber() {
541: Locator locator = getCurrentDigester().getDocumentLocator();
542: if (locator != null) {
543: return locator.getLineNumber();
544: } else {
545: return 0;
546: }
547: }
548:
549: private static Digester currentDigester;
550:
551: protected static void setCurrentDigester(Digester newDigester) {
552: currentDigester = newDigester;
553: }
554:
555: protected static void clearCurrentDigester() {
556: currentDigester = null;
557: }
558:
559: protected static Digester getCurrentDigester() {
560: // try to prevent invalid access to nonexistent digester instance
561: if (currentDigester == null) {
562: throw new IllegalStateException("current digester is null");
563: }
564:
565: return currentDigester;
566: }
567:
568: private static String currentFilename;
569:
570: protected void setCurrentFilename(String newFilename) {
571: currentFilename = newFilename;
572: }
573:
574: protected void clearCurrentFilename() {
575: currentFilename = null;
576: }
577:
578: protected boolean getExceptionsOccurred() {
579: return exceptionsOccurred;
580: }
581:
582: protected void setExceptionsOccurred(boolean exceptionsOccured) {
583: this .exceptionsOccurred = exceptionsOccured;
584: }
585:
586: public void setKualiConfigurationService(
587: KualiConfigurationService kualiConfigurationService) {
588: this .kualiConfigurationService = kualiConfigurationService;
589: }
590:
591: public KualiConfigurationService getKualiConfigurationService() {
592: return kualiConfigurationService;
593: }
594:
595: public void setKualiGroupService(KualiGroupService kualiGroupService) {
596: this .kualiGroupService = kualiGroupService;
597: }
598:
599: public KualiGroupService getKualiGroupService() {
600: return kualiGroupService;
601: }
602: }
|