001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.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/ecl2.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 */
016package org.kuali.rice.krad.datadictionary;
017
018import java.beans.PropertyDescriptor;
019import java.io.File;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.TreeMap;
029
030import org.apache.commons.beanutils.PropertyUtils;
031import org.apache.commons.collections.ListUtils;
032import org.apache.commons.lang3.ArrayUtils;
033import org.apache.commons.lang.ClassUtils;
034import org.apache.commons.lang.StringUtils;
035import org.kuali.rice.core.api.config.property.ConfigContext;
036import org.kuali.rice.core.api.util.ClassLoaderUtils;
037import org.kuali.rice.krad.data.provider.annotation.UifAutoCreateViewType;
038import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
039import org.kuali.rice.krad.datadictionary.exception.CompletionException;
040import org.kuali.rice.krad.datadictionary.parse.StringListConverter;
041import org.kuali.rice.krad.datadictionary.parse.StringMapConverter;
042import org.kuali.rice.krad.datadictionary.uif.ComponentBeanPostProcessor;
043import org.kuali.rice.krad.datadictionary.uif.UifBeanFactoryPostProcessor;
044import org.kuali.rice.krad.datadictionary.uif.UifDictionaryIndex;
045import org.kuali.rice.krad.datadictionary.validator.ErrorReport;
046import org.kuali.rice.krad.datadictionary.validator.ValidationTrace;
047import org.kuali.rice.krad.datadictionary.validator.Validator;
048import org.kuali.rice.krad.lookup.LookupView;
049import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
050import org.kuali.rice.krad.service.LegacyDataAdapter;
051import org.kuali.rice.krad.uif.UifConstants;
052import org.kuali.rice.krad.uif.UifConstants.ViewType;
053import org.kuali.rice.krad.uif.util.ComponentFactory;
054import org.kuali.rice.krad.uif.util.ExpressionFunctions;
055import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
056import org.kuali.rice.krad.uif.view.InquiryView;
057import org.kuali.rice.krad.uif.view.View;
058import org.kuali.rice.krad.util.KRADConstants;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061import org.springframework.beans.PropertyValue;
062import org.springframework.beans.PropertyValues;
063import org.springframework.beans.factory.config.BeanDefinition;
064import org.springframework.beans.factory.config.BeanExpressionContext;
065import org.springframework.beans.factory.config.BeanPostProcessor;
066import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
067import org.springframework.beans.factory.config.Scope;
068import org.springframework.beans.factory.support.ChildBeanDefinition;
069import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
070import org.springframework.context.expression.StandardBeanExpressionResolver;
071import org.springframework.core.convert.support.GenericConversionService;
072import org.springframework.core.io.DefaultResourceLoader;
073import org.springframework.core.io.Resource;
074import org.springframework.expression.spel.support.StandardEvaluationContext;
075import org.springframework.util.StopWatch;
076
077/**
078 * Encapsulates a bean factory and indexes to the beans within the factory for providing
079 * framework metadata
080 *
081 * @author Kuali Rice Team (rice.collab@kuali.org)
082 */
083public class DataDictionary {
084
085    private static final Logger LOG = LoggerFactory.getLogger(DataDictionary.class);
086
087    protected static boolean validateEBOs = true;
088
089    protected DefaultListableBeanFactory ddBeans = new DefaultListableBeanFactory();
090    protected XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ddBeans);
091
092    protected DataDictionaryIndex ddIndex = new DataDictionaryIndex(ddBeans);
093    protected UifDictionaryIndex uifIndex = new UifDictionaryIndex(ddBeans);
094
095    protected DataDictionaryMapper ddMapper = new DataDictionaryIndexMapper();
096
097    protected Map<String, List<String>> moduleDictionaryFiles = new HashMap<String, List<String>>();
098    protected List<String> moduleLoadOrder = new ArrayList<String>();
099
100    protected ArrayList<String> beanValidationFiles = new ArrayList<String>();
101
102    public static LegacyDataAdapter legacyDataAdapter;
103
104    protected transient StopWatch timer;
105
106    /**
107     * Populates and processes the dictionary bean factory based on the configured files and
108     * performs indexing
109     *
110     * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread
111     * or the same thread
112     */
113    public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) {
114        timer = new StopWatch("DD Processing");
115        setupProcessor(ddBeans);
116
117        loadDictionaryBeans(ddBeans, moduleDictionaryFiles, ddIndex, beanValidationFiles);
118
119        performDictionaryPostProcessing(allowConcurrentValidation);
120    }
121
122    /**
123     * Sets up the bean post processor and conversion service
124     *
125     * @param beans - The bean factory for the the dictionary beans
126     */
127    public static void setupProcessor(DefaultListableBeanFactory beans) {
128        try {
129            // UIF post processor that sets component ids
130            BeanPostProcessor idPostProcessor = ComponentBeanPostProcessor.class.newInstance();
131            beans.addBeanPostProcessor(idPostProcessor);
132            beans.setBeanExpressionResolver(new StandardBeanExpressionResolver() {
133                @Override
134                protected void customizeEvaluationContext(StandardEvaluationContext evalContext) {
135                    try {
136                        evalContext.registerFunction("getService", ExpressionFunctions.class.getDeclaredMethod("getService", new Class[]{String.class}));
137                    } catch(NoSuchMethodException me) {
138                        LOG.error("Unable to register custom expression to data dictionary bean factory", me);
139                    }
140                }
141            });
142
143            // special converters for shorthand map and list property syntax
144            GenericConversionService conversionService = new GenericConversionService();
145            conversionService.addConverter(new StringMapConverter());
146            conversionService.addConverter(new StringListConverter());
147
148            beans.setConversionService(conversionService);
149        } catch (Exception e1) {
150            throw new DataDictionaryException("Cannot create component decorator post processor: " + e1.getMessage(),
151                    e1);
152        }
153    }
154
155    /**
156     * Populates and processes the dictionary bean factory based on the configured files
157     *
158     * @param beans - The bean factory for the dictionary bean
159     * @param moduleDictionaryFiles - List of bean xml files
160     * @param index - Index of the data dictionary beans
161     * @param validationFiles - The List of bean xml files loaded into the bean file
162     */
163    public void loadDictionaryBeans(DefaultListableBeanFactory beans,
164            Map<String, List<String>> moduleDictionaryFiles, DataDictionaryIndex index,
165            ArrayList<String> validationFiles) {
166        // expand configuration locations into files
167        timer.start("XML File Loading");
168        LOG.info("Starting DD XML File Load");
169
170        List<String> allBeanNames = new ArrayList<String>();
171        for (String namespaceCode : moduleLoadOrder) {
172            LOG.info( "Processing Module: " + namespaceCode);
173            List<String> moduleDictionaryLocations = moduleDictionaryFiles.get(namespaceCode);
174            if ( LOG.isDebugEnabled() ) {
175                LOG.debug("DD Locations in Module: " + moduleDictionaryLocations);
176            }
177
178            if (moduleDictionaryLocations == null) {
179               continue;
180            }
181
182            XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(beans);
183
184            String configFileLocationsArray[] = new String[moduleDictionaryLocations.size()];
185            configFileLocationsArray = moduleDictionaryLocations.toArray(configFileLocationsArray);
186            for (int i = 0; i < configFileLocationsArray.length; i++) {
187                validationFiles.add(configFileLocationsArray[i]);
188            }
189
190            try {
191                xmlReader.loadBeanDefinitions(configFileLocationsArray);
192
193                // get updated bean names from factory and compare to our previous list to get those that
194                // were added by the last namespace
195                List<String> addedBeanNames = Arrays.asList(beans.getBeanDefinitionNames());
196                addedBeanNames = ListUtils.removeAll(addedBeanNames, allBeanNames);
197                index.addBeanNamesToNamespace(namespaceCode, addedBeanNames);
198
199                allBeanNames.addAll(addedBeanNames);
200            } catch (Exception e) {
201                throw new DataDictionaryException("Error loading bean definitions: " + e.getLocalizedMessage(),e);
202            }
203        }
204
205        LOG.info("Completed DD XML File Load");
206        timer.stop();
207    }
208
209    /**
210     * Invokes post processors and builds indexes for the beans contained in the dictionary
211     *
212     * @param allowConcurrentValidation - indicates whether the indexing should occur on a different thread
213     * or the same thread
214     */
215    public void performDictionaryPostProcessing(boolean allowConcurrentValidation) {
216        LOG.info("Starting Data Dictionary Post Processing");
217
218        timer.start("Spring Post Processing");
219        PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer();
220        propertyPlaceholderConfigurer.setProperties(ConfigContext.getCurrentContextConfig().getProperties());
221        propertyPlaceholderConfigurer.postProcessBeanFactory(ddBeans);
222
223        DictionaryBeanFactoryPostProcessor dictionaryBeanPostProcessor =
224                new DictionaryBeanFactoryPostProcessor(DataDictionary.this, ddBeans);
225        dictionaryBeanPostProcessor.postProcessBeanFactory();
226        timer.stop();
227
228        // post processes UIF beans for pulling out expressions within property values
229        timer.start("UIF Post Processing");
230        UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor();
231        factoryPostProcessor.postProcessBeanFactory(ddBeans);
232        timer.stop();
233
234        timer.start("Instantiating DD Beans");
235        ddBeans.preInstantiateSingletons();
236        timer.stop();
237
238        // Allow the DD to perform final post processing in a controlled order
239        // Unlike the Spring post processor, we will only call for these operations on the
240        // "top-level" beans and have them call post processing actions on embedded DD objects, if needed
241        timer.start("DD Post Processing");
242        
243        for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) {
244            entry.dataDictionaryPostProcessing();
245        }
246        
247        for (DocumentEntry entry : ddBeans.getBeansOfType(DocumentEntry.class).values()) {
248            entry.dataDictionaryPostProcessing();
249        }
250        
251        timer.stop();
252
253        timer.start("Data Dictionary Indexing");
254        ddIndex.run();
255        timer.stop();
256
257        // the UIF defaulting must be done before the UIF indexing but after the main DD data object indexing
258        if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KRADConstants.Config.ENABLE_VIEW_AUTOGENERATION, false)) {
259            timer.start("UIF Defaulting");
260            generateMissingInquiryDefinitions();
261            generateMissingLookupDefinitions();
262            timer.stop();
263        }
264
265        timer.start("UIF Indexing");
266        uifIndex.run();
267        timer.stop();
268
269        LOG.info("Completed Data Dictionary Post Processing");
270    }
271
272    protected void generateMissingInquiryDefinitions() {
273        Collection<InquiryView> inquiryViewBeans = ddBeans.getBeansOfType(InquiryView.class).values();
274        
275        // Index all the inquiry views by the data object class so we can find them easily below
276        Map<Class<?>,InquiryView> defaultViewsByDataObjectClass = new HashMap<Class<?>, InquiryView>();
277        
278        for ( InquiryView view : inquiryViewBeans ) {
279            if ( view.getViewName().equals(UifConstants.DEFAULT_VIEW_NAME) ) {
280                defaultViewsByDataObjectClass.put(view.getDataObjectClassName(), view);
281            }
282        }
283        
284        for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) {
285            // if an inquiry already exists, just ignore - we only default if none exist
286            if ( defaultViewsByDataObjectClass.containsKey(entry.getDataObjectClass())) {
287                continue;
288            }
289            
290            // We only generate the inquiry if the metadata says to
291            if ( entry.getDataObjectMetadata() == null ) {
292                continue;
293            }
294            
295            if ( !entry.getDataObjectMetadata().shouldAutoCreateUifViewOfType(UifAutoCreateViewType.INQUIRY)) {
296                continue;
297            }
298            
299            // no inquiry exists and we want one to, create one
300            if ( LOG.isInfoEnabled() ) {
301                LOG.info( "Generating Inquiry View for : " + entry.getDataObjectClass() );
302            }
303            
304            String inquiryBeanName = entry.getDataObjectClass().getSimpleName()+"-InquiryView-default";
305
306            InquiryView inquiryView = KRADServiceLocatorWeb.getUifDefaultingService().deriveInquiryViewFromMetadata(entry);
307            inquiryView.setId(inquiryBeanName);
308            inquiryView.setViewName(UifConstants.DEFAULT_VIEW_NAME);
309
310            ChildBeanDefinition inquiryBean = new ChildBeanDefinition("Uif-InquiryView");
311            inquiryBean.setScope(BeanDefinition.SCOPE_SINGLETON);
312            inquiryBean.setAttribute("dataObjectClassName", inquiryView.getDataObjectClassName());
313            inquiryBean.getPropertyValues().add("dataObjectClassName", inquiryView.getDataObjectClassName().getName());
314            inquiryBean.setResourceDescription("Autogenerated From Metadata");
315            ddBeans.registerBeanDefinition(inquiryBeanName, inquiryBean);
316            ddBeans.registerSingleton(inquiryBeanName, inquiryView);
317        }
318    }
319
320    protected void generateMissingLookupDefinitions() {
321        Collection<LookupView> lookupViewBeans = ddBeans.getBeansOfType(LookupView.class).values();
322        // Index all the inquiry views by the data object class so we can find them easily below
323        Map<Class<?>,LookupView> defaultViewsByDataObjectClass = new HashMap<Class<?>, LookupView>();
324        for ( LookupView view : lookupViewBeans ) {
325            if ( view.getViewName().equals(UifConstants.DEFAULT_VIEW_NAME) ) {
326                defaultViewsByDataObjectClass.put(view.getDataObjectClass(), view);
327            }
328        }
329        for (DataObjectEntry entry : ddBeans.getBeansOfType(DataObjectEntry.class).values()) {
330            // if an inquiry already exists, just ignore - we only default if none exist
331            if ( defaultViewsByDataObjectClass.containsKey(entry.getDataObjectClass())) {
332                continue;
333            }
334            // We only generate the inquiry if the metadata says to
335            if ( entry.getDataObjectMetadata() == null ) {
336                continue;
337            }
338            if ( !entry.getDataObjectMetadata().shouldAutoCreateUifViewOfType(UifAutoCreateViewType.LOOKUP)) {
339                continue;
340            }
341            // no inquiry exists and we want one to, create one
342            if ( LOG.isInfoEnabled() ) {
343                LOG.info( "Generating Lookup View for : " + entry.getDataObjectClass() );
344            }
345            String lookupBeanName = entry.getDataObjectClass().getSimpleName()+"-LookupView-default";
346
347            LookupView lookupView = KRADServiceLocatorWeb.getUifDefaultingService().deriveLookupViewFromMetadata(entry);
348            lookupView.setId(lookupBeanName);
349            lookupView.setViewName(UifConstants.DEFAULT_VIEW_NAME);
350
351            ChildBeanDefinition lookupBean = new ChildBeanDefinition(ComponentFactory.LOOKUP_VIEW);
352            lookupBean.setScope(BeanDefinition.SCOPE_SINGLETON);
353            lookupBean.setAttribute("dataObjectClassName", lookupView.getDataObjectClass());
354            lookupBean.getPropertyValues().add("dataObjectClassName", lookupView.getDataObjectClass().getName());
355            lookupBean.setResourceDescription("Autogenerated From Metadata");
356            ddBeans.registerBeanDefinition(lookupBeanName, lookupBean);
357            ddBeans.registerSingleton(lookupBeanName, lookupView);
358        }
359    }
360
361    public void validateDD(boolean validateEbos) {
362        timer.start("Validation");
363        DataDictionary.validateEBOs = validateEbos;
364
365        Validator.resetErrorReport();
366
367        Map<String, DataObjectEntry> doBeans = ddBeans.getBeansOfType(DataObjectEntry.class);
368        for (DataObjectEntry entry : doBeans.values()) {
369            entry.completeValidation(new ValidationTrace());
370        }
371
372        Map<String, DocumentEntry> docBeans = ddBeans.getBeansOfType(DocumentEntry.class);
373        for (DocumentEntry entry : docBeans.values()) {
374            entry.completeValidation(new ValidationTrace());
375        }
376
377        List<ErrorReport> errorReports = Validator.getErrorReports();
378        if (!errorReports.isEmpty()) {
379            boolean hasErrors = hasErrors(errorReports);
380            String errorReport = produceErrorReport(errorReports, hasErrors);
381            if (hasErrors) {
382                String message = "Errors during DD validation, failing validation.\n" + errorReport;
383                throw new DataDictionaryException(message);
384            } else {
385                String message = "Warnings during DD validation.\n" + errorReport;
386                LOG.warn(message);
387            }
388        }
389
390        timer.stop();
391    }
392
393    private boolean hasErrors(List<ErrorReport> errorReports) {
394        for (ErrorReport err : errorReports) {
395            if (err.isError()) {
396                return true;
397            }
398        }
399        return false;
400    }
401
402    protected String produceErrorReport(List<ErrorReport> errorReports, boolean hasErrors) {
403        StringBuilder builder = new StringBuilder();
404        builder.append("***********************************************************\n");
405        if (hasErrors) {
406            builder.append("ERRORS REPORTED UPON DATA DICTIONARY VALIDATION\n");
407        } else {
408            builder.append("WARNINGS REPORTED UPON DATA DICTIONARY VALIDATION\n");
409        }
410        builder.append("***********************************************************\n");
411        for (ErrorReport report : errorReports) {
412            builder.append(report.errorMessage()).append("\n");
413        }
414        return builder.toString();
415    }
416
417    public void validateDD() {
418        validateDD(true);
419    }
420
421    /**
422     * Adds a location of files or a individual resource to the data dictionary
423     *
424     * <p>
425     * The location can either be an XML file on the classpath or a file or folder location within the
426     * file system. If a folder location is given, the folder and all sub-folders will be traversed and any
427     * XML files will be added to the dictionary
428     * </p>
429     *
430     * @param namespaceCode - namespace the beans loaded from the location should be associated with
431     * @param location - classpath resource or file system location
432     * @throws IOException
433     */
434    public void addConfigFileLocation(String namespaceCode, String location) throws IOException {
435        // add module to load order so we load in the order modules were configured
436        if (!moduleLoadOrder.contains(namespaceCode)) {
437            moduleLoadOrder.add(namespaceCode);
438        }
439
440        indexSource(namespaceCode, location);
441    }
442
443    /**
444     * Processes a given source for XML files to populate the dictionary with
445     *
446     * @param namespaceCode - namespace the beans loaded from the location should be associated with
447     * @param sourceName - a file system or classpath resource locator
448     * @throws IOException
449     */
450    protected void indexSource(String namespaceCode, String sourceName) throws IOException {
451        if (sourceName == null) {
452            throw new DataDictionaryException("Source Name given is null");
453        }
454
455        if (!sourceName.endsWith(".xml")) {
456            Resource resource = getFileResource(sourceName);
457            if (resource.exists()) {
458                try {
459                    indexSource(namespaceCode, resource.getFile());
460                } catch (IOException e) {
461                    // ignore resources that exist and cause an error here
462                    // they may be directories resident in jar files
463                    LOG.debug("Skipped existing resource without absolute file path");
464                }
465            } else {
466                LOG.warn("Could not find " + sourceName);
467                throw new DataDictionaryException("DD Resource " + sourceName + " not found");
468            }
469        } else {
470            if (LOG.isDebugEnabled()) {
471                LOG.debug("adding sourceName " + sourceName + " ");
472            }
473
474            Resource resource = getFileResource(sourceName);
475            if (!resource.exists()) {
476                throw new DataDictionaryException("DD Resource " + sourceName + " not found");
477            }
478
479            addModuleDictionaryFile(namespaceCode, sourceName);
480        }
481    }
482
483    protected Resource getFileResource(String sourceName) {
484        DefaultResourceLoader resourceLoader = new DefaultResourceLoader(ClassLoaderUtils.getDefaultClassLoader());
485
486        return resourceLoader.getResource(sourceName);
487    }
488
489    protected void indexSource(String namespaceCode, File dir) {
490        for (File file : dir.listFiles()) {
491            if (file.isDirectory()) {
492                indexSource(namespaceCode, file);
493            } else if (file.getName().endsWith(".xml")) {
494                addModuleDictionaryFile(namespaceCode, "file:" + file.getAbsolutePath());
495            } else {
496                if (LOG.isDebugEnabled()) {
497                    LOG.debug("Skipping non xml file " + file.getAbsolutePath() + " in DD load");
498                }
499            }
500        }
501    }
502
503    /**
504     * Adds a file location to the list of dictionary files for the given namespace code
505     *
506     * @param namespaceCode - namespace to add location for
507     * @param location - file or resource location to add
508     */
509    protected void addModuleDictionaryFile(String namespaceCode, String location) {
510        List<String> moduleFileLocations = new ArrayList<String>();
511        if (moduleDictionaryFiles.containsKey(namespaceCode)) {
512            moduleFileLocations = moduleDictionaryFiles.get(namespaceCode);
513        }
514        moduleFileLocations.add(location);
515
516        moduleDictionaryFiles.put(namespaceCode, moduleFileLocations);
517    }
518
519    /**
520     * Mapping of namespace codes to dictionary files that are associated with
521     * that namespace
522     *
523     * @return Map<String, List<String>> where map key is namespace code, and value is list of dictionary
524     *         file locations
525     */
526    public Map<String, List<String>> getModuleDictionaryFiles() {
527        return moduleDictionaryFiles;
528    }
529
530    /**
531     * Setter for the map of module dictionary files
532     *
533     * @param moduleDictionaryFiles
534     */
535    public void setModuleDictionaryFiles(Map<String, List<String>> moduleDictionaryFiles) {
536        this.moduleDictionaryFiles = moduleDictionaryFiles;
537    }
538
539    /**
540     * Order modules should be loaded into the dictionary
541     *
542     * <p>
543     * Modules are loaded in the order they are found in this list. If not explicity set, they will be loaded in
544     * the order their dictionary file locations are added
545     * </p>
546     *
547     * @return List<String> list of namespace codes indicating the module load order
548     */
549    public List<String> getModuleLoadOrder() {
550        return moduleLoadOrder;
551    }
552
553    /**
554     * Setter for the list of namespace codes indicating the module load order
555     *
556     * @param moduleLoadOrder
557     */
558    public void setModuleLoadOrder(List<String> moduleLoadOrder) {
559        this.moduleLoadOrder = moduleLoadOrder;
560    }
561
562    /**
563     * Sets the DataDictionaryMapper
564     *
565     * @param mapper the datadictionary mapper
566     */
567    public void setDataDictionaryMapper(DataDictionaryMapper mapper) {
568        this.ddMapper = mapper;
569    }
570
571    /**
572     * @param className
573     * @return BusinessObjectEntry for the named class, or null if none exists
574     */
575    @Deprecated
576    public BusinessObjectEntry getBusinessObjectEntry(String className) {
577        return ddMapper.getBusinessObjectEntry(ddIndex, className);
578    }
579
580    /**
581     * @param className
582     * @return BusinessObjectEntry for the named class, or null if none exists
583     */
584    public DataObjectEntry getDataObjectEntry(String className) {
585        return ddMapper.getDataObjectEntry(ddIndex, className);
586    }
587
588    /**
589     * This method gets the business object entry for a concrete class
590     *
591     * @param className
592     * @return business object entry
593     */
594    public BusinessObjectEntry getBusinessObjectEntryForConcreteClass(String className) {
595        return ddMapper.getBusinessObjectEntryForConcreteClass(ddIndex, className);
596    }
597
598    /**
599     * @return List of businessObject classnames
600     */
601    public List<String> getBusinessObjectClassNames() {
602        return ddMapper.getBusinessObjectClassNames(ddIndex);
603    }
604
605    /**
606     * @return Map of (classname, BusinessObjectEntry) pairs
607     */
608    public Map<String, BusinessObjectEntry> getBusinessObjectEntries() {
609        return ddMapper.getBusinessObjectEntries(ddIndex);
610    }
611
612    public Map<String, DataObjectEntry> getDataObjectEntries() {
613        return ddMapper.getDataObjectEntries(ddIndex);
614    }
615
616    /**
617     * @param className
618     * @return DataDictionaryEntryBase for the named class, or null if none
619     *         exists
620     */
621    public DataDictionaryEntry getDictionaryObjectEntry(String className) {
622        return ddMapper.getDictionaryObjectEntry(ddIndex, className);
623    }
624
625    /**
626     * Returns the KNS document entry for the given lookup key.  The documentTypeDDKey is interpreted
627     * successively in the following ways until a mapping is found (or none if found):
628     * <ol>
629     * <li>KEW/workflow document type</li>
630     * <li>business object class name</li>
631     * <li>maintainable class name</li>
632     * </ol>
633     * This mapping is compiled when DataDictionary files are parsed on startup (or demand).  Currently this
634     * means the mapping is static, and one-to-one (one KNS document maps directly to one and only
635     * one key).
636     *
637     * @param documentTypeDDKey the KEW/workflow document type name
638     * @return the KNS DocumentEntry if it exists
639     */
640    public DocumentEntry getDocumentEntry(String documentTypeDDKey) {
641        return ddMapper.getDocumentEntry(ddIndex, documentTypeDDKey);
642    }
643
644    /**
645     * Note: only MaintenanceDocuments are indexed by businessObject Class
646     *
647     * This is a special case that is referenced in one location. Do we need
648     * another map for this stuff??
649     *
650     * @param businessObjectClass
651     * @return DocumentEntry associated with the given Class, or null if there
652     *         is none
653     */
654    public MaintenanceDocumentEntry getMaintenanceDocumentEntryForBusinessObjectClass(Class<?> businessObjectClass) {
655        return ddMapper.getMaintenanceDocumentEntryForBusinessObjectClass(ddIndex, businessObjectClass);
656    }
657
658    public Map<String, DocumentEntry> getDocumentEntries() {
659        return ddMapper.getDocumentEntries(ddIndex);
660    }
661
662    /**
663     * Returns the View entry identified by the given id
664     *
665     * @param viewId unique id for view
666     * @return View instance associated with the id
667     */
668    public View getViewById(String viewId) {
669        return ddMapper.getViewById(uifIndex, viewId);
670    }
671
672    /**
673     * Returns the View entry identified by the given id, meant for view readonly
674     * access (not running the lifecycle but just checking configuration)
675     *
676     * @param viewId unique id for view
677     * @return View instance associated with the id
678     */
679    public View getImmutableViewById(String viewId) {
680        return ddMapper.getImmutableViewById(uifIndex, viewId);
681    }
682
683    /**
684     * Returns View instance identified by the view type name and index
685     *
686     * @param viewTypeName - type name for the view
687     * @param indexKey - Map of index key parameters, these are the parameters the
688     * indexer used to index the view initially and needs to identify
689     * an unique view instance
690     * @return View instance that matches the given index
691     */
692    public View getViewByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
693        return ddMapper.getViewByTypeIndex(uifIndex, viewTypeName, indexKey);
694    }
695
696    /**
697     * Returns the view id for the view that matches the given view type and index
698     *
699     * @param viewTypeName type name for the view
700     * @param indexKey Map of index key parameters, these are the parameters the
701     * indexer used to index the view initially and needs to identify
702     * an unique view instance
703     * @return id for the view that matches the view type and index or null if a match is not found
704     */
705    public String getViewIdByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
706        return ddMapper.getViewIdByTypeIndex(uifIndex, viewTypeName, indexKey);
707    }
708
709    /**
710     * Indicates whether a <code>View</code> exists for the given view type and index information
711     *
712     * @param viewTypeName - type name for the view
713     * @param indexKey - Map of index key parameters, these are the parameters the
714     * indexer used to index the view initially and needs to identify
715     * an unique view instance
716     * @return boolean true if view exists, false if not
717     */
718    public boolean viewByTypeExist(ViewType viewTypeName, Map<String, String> indexKey) {
719        return ddMapper.viewByTypeExist(uifIndex, viewTypeName, indexKey);
720    }
721
722    /**
723     * Gets all <code>View</code> prototypes configured for the given view type
724     * name
725     *
726     * @param viewTypeName - view type name to retrieve
727     * @return List<View> view prototypes with the given type name, or empty
728     *         list
729     */
730    public List<View> getViewsForType(ViewType viewTypeName) {
731        return ddMapper.getViewsForType(uifIndex, viewTypeName);
732    }
733
734    /**
735     * Returns an object from the dictionary by its spring bean name
736     *
737     * @param beanName id or name for the bean definition
738     * @return Object object instance created or the singleton being maintained
739     */
740    public Object getDictionaryBean(final String beanName) {
741        return ddBeans.getBean(beanName);
742    }
743
744    /**
745     * Indicates whether the data dictionary contains a bean with the given id
746     *
747     * @param id id of the bean to check for
748     * @return boolean true if dictionary contains bean, false otherwise
749     */
750    public boolean containsDictionaryBean(String id) {
751        return ddBeans.containsBean(id);
752    }
753
754    /**
755     * Returns a prototype object from the dictionary by its spring bean name
756     * 
757     * @param beanName id or name for the bean definition
758     * @return Object object instance created
759     */
760    public Object getDictionaryPrototype(final String beanName) {
761        if (!ddBeans.isPrototype(beanName)) {
762            throw new IllegalArgumentException("Bean name " + beanName
763                    + " doesn't refer to a prototype bean in the data dictionary");
764        }
765        
766        return getDictionaryBean(beanName);
767    }
768
769    /**
770     * Returns a property value for the bean with the given name from the dictionary.
771     *
772     * @param beanName id or name for the bean definition
773     * @param propertyName name of the property to retrieve, must be a valid property configured on
774     * the bean definition
775     * @return Object property value for property
776     */
777    public Object getDictionaryBeanProperty(String beanName, String propertyName) {
778        Object bean = ddBeans.getSingleton(beanName);
779        if (bean != null) {
780            return ObjectPropertyUtils.getPropertyValue(bean, propertyName);
781        }
782
783        BeanDefinition beanDefinition = ddBeans.getMergedBeanDefinition(beanName);
784
785        if (beanDefinition == null) {
786            throw new RuntimeException("Unable to get bean for bean name: " + beanName);
787        }
788
789        PropertyValues pvs = beanDefinition.getPropertyValues();
790        if (pvs.contains(propertyName)) {
791            PropertyValue propertyValue = pvs.getPropertyValue(propertyName);
792
793            Object value;
794            if (propertyValue.isConverted()) {
795                value = propertyValue.getConvertedValue();
796            } else if (propertyValue.getValue() instanceof String) {
797                String unconvertedValue = (String) propertyValue.getValue();
798                Scope scope = ddBeans.getRegisteredScope(beanDefinition.getScope());
799                BeanExpressionContext beanExpressionContext = new BeanExpressionContext(ddBeans, scope);
800
801                value = ddBeans.getBeanExpressionResolver().evaluate(unconvertedValue, beanExpressionContext);
802            } else {
803                value = propertyValue.getValue();
804            }
805
806            return value;
807        }
808
809        return null;
810    }
811
812    /**
813     * Retrieves the configured property values for the view bean definition associated with the given id
814     *
815     * <p>
816     * Since constructing the View object can be expensive, when metadata only is needed this method can be used
817     * to retrieve the configured property values. Note this looks at the merged bean definition
818     * </p>
819     *
820     * @param viewId - id for the view to retrieve
821     * @return PropertyValues configured on the view bean definition, or null if view is not found
822     */
823    public PropertyValues getViewPropertiesById(String viewId) {
824        return ddMapper.getViewPropertiesById(uifIndex, viewId);
825    }
826
827    /**
828     * Retrieves the configured property values for the view bean definition associated with the given type and
829     * index
830     *
831     * <p>
832     * Since constructing the View object can be expensive, when metadata only is needed this method can be used
833     * to retrieve the configured property values. Note this looks at the merged bean definition
834     * </p>
835     *
836     * @param viewTypeName - type name for the view
837     * @param indexKey - Map of index key parameters, these are the parameters the indexer used to index
838     * the view initially and needs to identify an unique view instance
839     * @return PropertyValues configured on the view bean definition, or null if view is not found
840     */
841    public PropertyValues getViewPropertiesByType(ViewType viewTypeName, Map<String, String> indexKey) {
842        return ddMapper.getViewPropertiesByType(uifIndex, viewTypeName, indexKey);
843    }
844
845    /**
846     * Retrieves the list of dictionary bean names that are associated with the given namespace code
847     *
848     * @param namespaceCode - namespace code to retrieve associated bean names for
849     * @return List<String> bean names associated with the namespace
850     */
851    public List<String> getBeanNamesForNamespace(String namespaceCode) {
852        List<String> namespaceBeans = new ArrayList<String>();
853
854        Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace();
855        if (dictionaryBeansByNamespace.containsKey(namespaceCode)) {
856            namespaceBeans = dictionaryBeansByNamespace.get(namespaceCode);
857        }
858
859        return namespaceBeans;
860    }
861
862    /**
863     * Retrieves the namespace code the given bean name is associated with
864     *
865     * @param beanName - name of the dictionary bean to find namespace code for
866     * @return String namespace code the bean is associated with, or null if a namespace was not found
867     */
868    public String getNamespaceForBeanDefinition(String beanName) {
869        String beanNamespace = null;
870
871        Map<String, List<String>> dictionaryBeansByNamespace = ddIndex.getDictionaryBeansByNamespace();
872        for (Map.Entry<String, List<String>> moduleDefinitions : dictionaryBeansByNamespace.entrySet()) {
873            List<String> namespaceBeans = moduleDefinitions.getValue();
874            if (namespaceBeans.contains(beanName)) {
875                beanNamespace = moduleDefinitions.getKey();
876                break;
877            }
878        }
879
880        return beanNamespace;
881    }
882
883    /**
884     * @param targetClass
885     * @param propertyName
886     * @return true if the given propertyName names a property of the given class
887     * @throws CompletionException if there is a problem accessing the named property on the given class
888     */
889    public static boolean isPropertyOf(Class targetClass, String propertyName) {
890        if (targetClass == null) {
891            throw new IllegalArgumentException("invalid (null) targetClass");
892        }
893        if (StringUtils.isBlank(propertyName)) {
894            throw new IllegalArgumentException("invalid (blank) propertyName");
895        }
896        try {
897            PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
898
899            return propertyDescriptor != null;
900        } catch ( Exception ex ) {
901            LOG.error( "Exception while obtaining property descriptor for " + targetClass.getName() + "." + propertyName, ex );
902            return false;
903        }
904    }
905
906    /**
907     * @param targetClass
908     * @param propertyName
909     * @return true if the given propertyName names a Collection property of the given class
910     * @throws CompletionException if there is a problem accessing the named property on the given class
911     */
912    public static boolean isCollectionPropertyOf(Class targetClass, String propertyName) {
913        boolean isCollectionPropertyOf = false;
914
915        PropertyDescriptor propertyDescriptor = buildReadDescriptor(targetClass, propertyName);
916        if (propertyDescriptor != null) {
917            Class clazz = propertyDescriptor.getPropertyType();
918
919            if ((clazz != null) && Collection.class.isAssignableFrom(clazz)) {
920                isCollectionPropertyOf = true;
921            }
922        }
923
924        return isCollectionPropertyOf;
925    }
926
927    public static LegacyDataAdapter getLegacyDataAdapter() {
928        if (legacyDataAdapter == null) {
929            legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter();
930        }
931        return legacyDataAdapter;
932    }
933
934    /**
935     * This method determines the Class of the attributeName passed in. Null will be returned if the member is not
936     * available, or if
937     * a reflection exception is thrown.
938     *
939     * @param boClass - Class that the attributeName property exists in.
940     * @param attributeName - Name of the attribute you want a class for.
941     * @return The Class of the attributeName, if the attribute exists on the rootClass. Null otherwise.
942     */
943    public static Class getAttributeClass(Class boClass, String attributeName) {
944
945        // fail loudly if the attributeName isnt a member of rootClass
946        if (!isPropertyOf(boClass, attributeName)) {
947            throw new AttributeValidationException(
948                    "unable to find attribute '" + attributeName + "' in rootClass '" + boClass.getName() + "'");
949        }
950
951        //Implementing Externalizable Business Object Services...
952        //The boClass can be an interface, hence handling this separately,
953        //since the original method was throwing exception if the class could not be instantiated.
954        if (boClass.isInterface()) {
955            return getAttributeClassWhenBOIsInterface(boClass, attributeName);
956        } else {
957            return getAttributeClassWhenBOIsClass(boClass, attributeName);
958        }
959
960    }
961
962    /**
963     * This method gets the property type of the given attributeName when the bo class is a concrete class
964     *
965     * @param boClass
966     * @param attributeName
967     * @return property type
968     */
969    private static Class<?> getAttributeClassWhenBOIsClass(Class<?> boClass, String attributeName) {
970        Object boInstance;
971        try {
972
973            //KULRICE-11351 should not differentiate between primitive types and their wrappers during DD validation
974            if (boClass.isPrimitive()) {
975                boClass = ClassUtils.primitiveToWrapper(boClass);
976            }
977
978            boInstance = boClass.newInstance();
979        } catch (Exception e) {
980            throw new RuntimeException("Unable to instantiate Data Object: " + boClass, e);
981        }
982
983        // attempt to retrieve the class of the property
984        try {
985            return getLegacyDataAdapter().getPropertyType(boInstance, attributeName);
986        } catch (Exception e) {
987            throw new RuntimeException(
988                    "Unable to determine property type for: " + boClass.getName() + "." + attributeName, e);
989        }
990    }
991
992    /**
993     * This method gets the property type of the given attributeName when the bo class is an interface
994     * This method will also work if the bo class is not an interface,
995     * but that case requires special handling, hence a separate method getAttributeClassWhenBOIsClass
996     *
997     * @param boClass
998     * @param attributeName
999     * @return property type
1000     */
1001    private static Class<?> getAttributeClassWhenBOIsInterface(Class<?> boClass, String attributeName) {
1002        if (boClass == null) {
1003            throw new IllegalArgumentException("invalid (null) boClass");
1004        }
1005        if (StringUtils.isBlank(attributeName)) {
1006            throw new IllegalArgumentException("invalid (blank) attributeName");
1007        }
1008
1009        PropertyDescriptor propertyDescriptor = null;
1010
1011        String[] intermediateProperties = attributeName.split("\\.");
1012        int lastLevel = intermediateProperties.length - 1;
1013        Class currentClass = boClass;
1014
1015        for (int i = 0; i <= lastLevel; ++i) {
1016
1017            String currentPropertyName = intermediateProperties[i];
1018            propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
1019
1020            if (propertyDescriptor != null) {
1021
1022                Class propertyType = propertyDescriptor.getPropertyType();
1023                if (getLegacyDataAdapter().isExtensionAttribute(currentClass, currentPropertyName, propertyType)) {
1024                    propertyType = getLegacyDataAdapter().getExtensionAttributeClass(currentClass, currentPropertyName);
1025                }
1026                if (Collection.class.isAssignableFrom(propertyType)) {
1027                    // TODO: determine property type using generics type definition
1028                    throw new AttributeValidationException(
1029                            "Can't determine the Class of Collection elements because when the business object is an (possibly ExternalizableBusinessObject) interface.");
1030                } else {
1031                    currentClass = propertyType;
1032                }
1033            } else {
1034                throw new AttributeValidationException(
1035                        "Can't find getter method of " + boClass.getName() + " for property " + attributeName);
1036            }
1037        }
1038        return currentClass;
1039    }
1040
1041    /**
1042     * This method determines the Class of the elements in the collectionName passed in.
1043     *
1044     * @param boClass Class that the collectionName collection exists in.
1045     * @param collectionName the name of the collection you want the element class for
1046     * @return collection element type
1047     */
1048    public static Class getCollectionElementClass(Class boClass, String collectionName) {
1049        if (boClass == null) {
1050            throw new IllegalArgumentException("invalid (null) boClass");
1051        }
1052        if (StringUtils.isBlank(collectionName)) {
1053            throw new IllegalArgumentException("invalid (blank) collectionName");
1054        }
1055
1056        PropertyDescriptor propertyDescriptor = null;
1057
1058        String[] intermediateProperties = collectionName.split("\\.");
1059        Class currentClass = boClass;
1060
1061        for (int i = 0; i < intermediateProperties.length; ++i) {
1062
1063            String currentPropertyName = intermediateProperties[i];
1064            propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
1065
1066            if (propertyDescriptor != null) {
1067
1068                Class type = propertyDescriptor.getPropertyType();
1069                if (Collection.class.isAssignableFrom(type)) {
1070                    currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName);
1071                } else {
1072                    currentClass = propertyDescriptor.getPropertyType();
1073                }
1074            }
1075        }
1076
1077        return currentClass;
1078    }
1079
1080    static private Map<String, Map<String, PropertyDescriptor>> cache =
1081            new TreeMap<String, Map<String, PropertyDescriptor>>();
1082
1083    /**
1084     * @param propertyClass
1085     * @param propertyName
1086     * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
1087     */
1088    public static PropertyDescriptor buildReadDescriptor(Class propertyClass, String propertyName) {
1089        if (propertyClass == null) {
1090            throw new IllegalArgumentException("invalid (null) propertyClass");
1091        }
1092        if (StringUtils.isBlank(propertyName)) {
1093            throw new IllegalArgumentException("invalid (blank) propertyName");
1094        }
1095
1096        PropertyDescriptor propertyDescriptor = null;
1097
1098        String[] intermediateProperties = propertyName.split("\\.");
1099        int lastLevel = intermediateProperties.length - 1;
1100        Class currentClass = propertyClass;
1101
1102        for (int i = 0; i <= lastLevel; ++i) {
1103
1104            String currentPropertyName = intermediateProperties[i];
1105            propertyDescriptor = buildSimpleReadDescriptor(currentClass, currentPropertyName);
1106
1107            if (i < lastLevel) {
1108
1109                if (propertyDescriptor != null) {
1110
1111                    Class propertyType = propertyDescriptor.getPropertyType();
1112                    if (getLegacyDataAdapter().isExtensionAttribute(currentClass, currentPropertyName, propertyType)) {
1113                        propertyType = getLegacyDataAdapter().getExtensionAttributeClass(currentClass,
1114                                currentPropertyName);
1115                    }
1116                    if (Collection.class.isAssignableFrom(propertyType)) {
1117                        currentClass = getLegacyDataAdapter().determineCollectionObjectType(currentClass, currentPropertyName);
1118                    } else {
1119                        currentClass = propertyType;
1120                    }
1121
1122                }
1123
1124            }
1125
1126        }
1127
1128        return propertyDescriptor;
1129    }
1130
1131    /**
1132     * @param propertyClass
1133     * @param propertyName
1134     * @return PropertyDescriptor for the getter for the named property of the given class, if one exists.
1135     */
1136    public static PropertyDescriptor buildSimpleReadDescriptor(Class propertyClass, String propertyName) {
1137        if (propertyClass == null) {
1138            throw new IllegalArgumentException("invalid (null) propertyClass");
1139        }
1140        if (StringUtils.isBlank(propertyName)) {
1141            throw new IllegalArgumentException("invalid (blank) propertyName");
1142        }
1143
1144        PropertyDescriptor p = null;
1145
1146        // check to see if we've cached this descriptor already. if yes, return true.
1147        String propertyClassName = propertyClass.getName();
1148        Map<String, PropertyDescriptor> m = cache.get(propertyClassName);
1149        if (null != m) {
1150            p = m.get(propertyName);
1151            if (null != p) {
1152                return p;
1153            }
1154        }
1155
1156        // Use PropertyUtils.getPropertyDescriptors instead of manually constructing PropertyDescriptor because of
1157        // issues with introspection and generic/co-variant return types
1158        // See https://issues.apache.org/jira/browse/BEANUTILS-340 for more details
1159
1160        PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(propertyClass);
1161        if (ArrayUtils.isNotEmpty(descriptors)) {
1162            for (PropertyDescriptor descriptor : descriptors) {
1163                if (descriptor.getName().equals(propertyName)) {
1164                    p = descriptor;
1165                }
1166            }
1167        }
1168
1169        // cache the property descriptor if we found it.
1170        if (p != null) {
1171            if (m == null) {
1172                m = new TreeMap<String, PropertyDescriptor>();
1173                cache.put(propertyClassName, m);
1174            }
1175            m.put(propertyName, p);
1176        }
1177
1178        return p;
1179    }
1180
1181    public Set<InactivationBlockingMetadata> getAllInactivationBlockingMetadatas(Class blockedClass) {
1182        return ddMapper.getAllInactivationBlockingMetadatas(ddIndex, blockedClass);
1183    }
1184
1185    /**
1186     * This method gathers beans of type BeanOverride and invokes each one's performOverride() method.
1187     */
1188    // KULRICE-4513
1189    public void performBeanOverrides() {
1190        timer.start("Processing BeanOverride beans");
1191        Collection<BeanOverride> beanOverrides = ddBeans.getBeansOfType(BeanOverride.class).values();
1192
1193        if (beanOverrides.isEmpty()) {
1194            LOG.info("DataDictionary.performOverrides(): No beans to override");
1195        }
1196        for (BeanOverride beanOverride : beanOverrides) {
1197
1198            Object bean = ddBeans.getBean(beanOverride.getBeanName());
1199            beanOverride.performOverride(bean);
1200            LOG.info("DataDictionary.performOverrides(): Performing override on bean: " + bean.toString());
1201        }
1202        timer.stop();
1203        // This is the last hook we have upon startup, so pretty-print the results here
1204        LOG.info( "\n" + timer.prettyPrint() );
1205    }
1206
1207}