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