001/**
002 * Copyright 2005-2015 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 org.apache.commons.lang.StringUtils;
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.krad.service.DataDictionaryService;
023import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
024import org.springframework.beans.BeansException;
025import org.springframework.beans.MutablePropertyValues;
026import org.springframework.beans.PropertyValue;
027import org.springframework.beans.factory.config.BeanDefinition;
028import org.springframework.beans.factory.config.BeanDefinitionHolder;
029import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
030import org.springframework.beans.factory.config.TypedStringValue;
031import org.springframework.beans.factory.support.BeanDefinitionRegistry;
032import org.springframework.beans.factory.support.ManagedArray;
033import org.springframework.beans.factory.support.ManagedList;
034import org.springframework.beans.factory.support.ManagedMap;
035import org.springframework.beans.factory.support.ManagedSet;
036
037import java.util.ArrayList;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.Stack;
042
043/**
044 * Post processor for the data dictionary bean factory
045 *
046 * <p>
047 * The 'driver' for other post processors. Essentially this iterates through each bean and its properties,
048 * making calls to the message and expression processors
049 * </p>
050 *
051 * @author Kuali Rice Team (rice.collab@kuali.org)
052 */
053public class DictionaryBeanFactoryPostProcessor {
054    private static final Log LOG = LogFactory.getLog(DictionaryBeanFactoryPostProcessor.class);
055
056    private DataDictionary dataDictionary;
057    private ConfigurableListableBeanFactory beanFactory;
058
059    private List<DictionaryBeanProcessor> beanProcessors;
060
061    /**
062     * Constructs a new processor for the given data dictionary and bean factory
063     *
064     * @param dataDictionary data dictionary instance that contains the bean factory
065     * @param beanFactory bean factory to process
066     */
067    public DictionaryBeanFactoryPostProcessor(DataDictionary dataDictionary,
068            ConfigurableListableBeanFactory beanFactory) {
069        this.dataDictionary = dataDictionary;
070        this.beanFactory = beanFactory;
071
072        this.beanProcessors = new ArrayList<DictionaryBeanProcessor>();
073        this.beanProcessors.add(new MessageBeanProcessor(dataDictionary, beanFactory));
074    }
075
076    /**
077     * Iterates through all beans in the factory and invokes processing of root bean definitions
078     *
079     * @throws org.springframework.beans.BeansException
080     */
081    public void postProcessBeanFactory() throws BeansException {
082        // check whether loading of external messages is enabled
083        boolean loadExternalMessages = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsBoolean(
084                "load.dictionary.external.messages");
085        if (!loadExternalMessages) {
086            return;
087        }
088
089        LOG.info("Beginning dictionary bean post processing");
090
091        List<DictionaryBeanProcessor> beanProcessors = new ArrayList<DictionaryBeanProcessor>();
092        beanProcessors.add(new MessageBeanProcessor(dataDictionary, beanFactory));
093
094        String[] beanNames = beanFactory.getBeanDefinitionNames();
095        for (int i = 0; i < beanNames.length; i++) {
096            String beanName = beanNames[i];
097            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
098
099            processRootBeanDefinition(beanName, beanDefinition);
100        }
101
102        LOG.info("Finished dictionary bean post processing");
103    }
104
105    /**
106     * Invokes processors to handle the root bean definition then processes the bean properties
107     *
108     * @param beanName name of the bean within the factory
109     * @param beanDefinition root bean definition to process
110     */
111    protected void processRootBeanDefinition(String beanName, BeanDefinition beanDefinition) {
112        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
113            beanProcessor.processRootBeanDefinition(beanName, beanDefinition);
114        }
115
116        Stack<BeanDefinitionHolder> nestedBeanStack = new Stack<BeanDefinitionHolder>();
117        nestedBeanStack.push(new BeanDefinitionHolder(beanDefinition, beanName));
118
119        processBeanProperties(beanDefinition, nestedBeanStack);
120    }
121
122    /**
123     * Invokes the processors to handle the given nested bean definition
124     *
125     * <p>
126     * A check is also made to determine if the nested bean has a non-generated id which is not registered in the
127     * factory, if so the bean is added as a registered bean (so it can be found by id)
128     * </p>
129     *
130     * @param beanName name of the nested bean definition in the bean factory
131     * @param beanDefinition nested bean definition to process
132     * @param nestedPropertyPath the property path to the nested bean from the parent bean definition
133     * @param isCollectionBean indicates whether the nested bean is in a collection, if so a different handler
134     * method is called on the processors
135     * @param nestedBeanStack the stack of bean containers(those beans which contain the bean)
136     */
137    public void processNestedBeanDefinition(String beanName, BeanDefinition beanDefinition, String nestedPropertyPath,
138            boolean isCollectionBean, Stack<BeanDefinitionHolder> nestedBeanStack) {
139        // if bean name is given and factory does not have it registered we need to add it (inner beans that
140        // were given an id)
141        if (StringUtils.isNotBlank(beanName) && !StringUtils.contains(beanName, "$") && !StringUtils.contains(beanName,
142                "#") && !beanFactory.containsBean(beanName)) {
143            ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(beanName, beanDefinition);
144        }
145
146        // invoke the processors to handle the nested bean
147        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
148            if (isCollectionBean) {
149                beanProcessor.processCollectionBeanDefinition(beanName, beanDefinition, nestedPropertyPath,
150                        nestedBeanStack);
151            } else {
152                beanProcessor.processNestedBeanDefinition(beanName, beanDefinition, nestedPropertyPath,
153                        nestedBeanStack);
154            }
155        }
156
157        BeanDefinitionHolder nestedBeanDefinitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
158        nestedBeanStack.push(nestedBeanDefinitionHolder);
159
160        processBeanProperties(beanDefinition, nestedBeanStack);
161
162        nestedBeanStack.pop();
163    }
164
165    /**
166     * Invokes the processors to handle the string value (which may be changed)
167     *
168     * @param propertyName name of the property that is being processed
169     * @param propertyValue the string property value to process
170     * @param nestedBeanStack the stack of bean containers, including the bean that contains the property
171     * @return String new property value (possibly modified by processors)
172     */
173    protected String processStringPropertyValue(String propertyName, String propertyValue,
174            Stack<BeanDefinitionHolder> nestedBeanStack) {
175        String processedStringValue = propertyValue;
176
177        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
178            processedStringValue = beanProcessor.processStringPropertyValue(propertyName, processedStringValue,
179                    nestedBeanStack);
180        }
181
182        return processedStringValue;
183    }
184
185    /**
186     * Invokes the processors to handle an array string value (which may be changed)
187     *
188     * @param propertyName name of the property that is being processed
189     * @param propertyValue the array which contains the string
190     * @param elementValue the string element value
191     * @param elementIndex the index of the string within the array
192     * @param nestedBeanStack the stack of bean containers, including the bean that contains the property
193     * @return String new property value (possibly modified by processors)
194     */
195    protected String processArrayStringPropertyValue(String propertyName, Object[] propertyValue, String elementValue,
196            int elementIndex, Stack<BeanDefinitionHolder> nestedBeanStack,
197            List<DictionaryBeanProcessor> beanProcessors) {
198        String processedStringValue = elementValue;
199
200        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
201            processedStringValue = beanProcessor.processArrayStringPropertyValue(propertyName, propertyValue,
202                    elementValue, elementIndex, nestedBeanStack);
203        }
204
205        return processedStringValue;
206    }
207
208    /**
209     * Invokes the processors to handle an list string value (which may be changed)
210     *
211     * @param propertyName name of the property that is being processed
212     * @param propertyValue the list which contains the string
213     * @param elementValue the string element value
214     * @param elementIndex the index of the string within the list
215     * @param nestedBeanStack the stack of bean containers, including the bean that contains the property
216     * @return String new property value (possibly modified by processors)
217     */
218    protected String processListStringPropertyValue(String propertyName, List<?> propertyValue, String elementValue,
219            int elementIndex, Stack<BeanDefinitionHolder> nestedBeanStack,
220            List<DictionaryBeanProcessor> beanProcessors) {
221        String processedStringValue = elementValue;
222
223        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
224            processedStringValue = beanProcessor.processListStringPropertyValue(propertyName, propertyValue,
225                    elementValue, elementIndex, nestedBeanStack);
226        }
227
228        return processedStringValue;
229    }
230
231    /**
232     * Invokes the processors to handle an set string value (which may be changed)
233     *
234     * @param propertyName name of the property that is being processed
235     * @param propertyValue the set which contains the string
236     * @param elementValue the string element value
237     * @param nestedBeanStack the stack of bean containers, including the bean that contains the property
238     * @return String new property value (possibly modified by processors)
239     */
240    protected String processSetStringPropertyValue(String propertyName, Set<?> propertyValue, String elementValue,
241            Stack<BeanDefinitionHolder> nestedBeanStack, List<DictionaryBeanProcessor> beanProcessors) {
242        String processedStringValue = elementValue;
243
244        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
245            processedStringValue = beanProcessor.processSetStringPropertyValue(propertyName, propertyValue,
246                    elementValue, nestedBeanStack);
247        }
248
249        return processedStringValue;
250    }
251
252    /**
253     * Invokes the processors to handle an map string value (which may be changed)
254     *
255     * @param propertyName name of the property that is being processed
256     * @param propertyValue the map which contains the string
257     * @param elementValue the string element value
258     * @param elementKey the key for the string within the map
259     * @param nestedBeanStack the stack of bean containers, including the bean that contains the property
260     * @return String new property value (possibly modified by processors)
261     */
262    protected String processMapStringPropertyValue(String propertyName, Map<?, ?> propertyValue, String elementValue,
263            Object elementKey, Stack<BeanDefinitionHolder> nestedBeanStack,
264            List<DictionaryBeanProcessor> beanProcessors) {
265        String processedStringValue = elementValue;
266
267        for (DictionaryBeanProcessor beanProcessor : beanProcessors) {
268            processedStringValue = beanProcessor.processMapStringPropertyValue(propertyName, propertyValue,
269                    elementValue, elementKey, nestedBeanStack);
270        }
271
272        return processedStringValue;
273    }
274
275    /**
276     * Iterates through the properties defined for the bean definition and invokes helper methods to process
277     * the property value
278     *
279     * @param beanDefinition bean definition whose properties will be processed
280     * @param nestedBeanStack stack of beans which contain the given bean
281     */
282    protected void processBeanProperties(BeanDefinition beanDefinition, Stack<BeanDefinitionHolder> nestedBeanStack) {
283        // iterate through properties and check for any configured message keys within the value
284        MutablePropertyValues pvs = beanDefinition.getPropertyValues();
285        PropertyValue[] pvArray = pvs.getPropertyValues();
286        for (PropertyValue pv : pvArray) {
287            Object newPropertyValue = null;
288            if (isStringValue(pv.getValue())) {
289                newPropertyValue = processStringPropertyValue(pv.getName(), getString(pv.getValue()), nestedBeanStack);
290            } else {
291                newPropertyValue = visitPropertyValue(pv.getName(), pv.getValue(), nestedBeanStack);
292            }
293
294            pvs.removePropertyValue(pv.getName());
295            pvs.addPropertyValue(pv.getName(), newPropertyValue);
296        }
297    }
298
299    /**
300     * Determines if the property value is a bean or collection, and calls the appropriate helper method
301     * to process further. Ultimately this invokes the processors to modify the property value if necessary
302     *
303     * @param propertyName name for the property being processed
304     * @param propertyValue value for the property to process
305     * @param nestedBeanStack stack of beans which contain the property
306     * @return Object the new property value (which may be modified0
307     */
308    protected Object visitPropertyValue(String propertyName, Object propertyValue,
309            Stack<BeanDefinitionHolder> nestedBeanStack) {
310        if (isBeanDefinitionValue(propertyValue)) {
311            String beanName = getBeanName(propertyValue);
312            BeanDefinition beanDefinition = getBeanDefinition(propertyValue);
313
314            processNestedBeanDefinition(beanName, beanDefinition, propertyName, false, nestedBeanStack);
315        } else if (isCollectionValue(propertyValue)) {
316            visitCollection(propertyValue, propertyName, nestedBeanStack);
317        }
318
319        return propertyValue;
320    }
321
322    /**
323     * Determines what kind of collection (or array) the given value is and call handlers based on the determined type
324     *
325     * @param value collection value to process
326     * @param propertyName name of the property which has the collection value
327     * @param nestedBeanStack stack of bean containers which contains the collection property
328     */
329    protected void visitCollection(Object value, String propertyName, Stack<BeanDefinitionHolder> nestedBeanStack) {
330        if (value instanceof Object[] || (value instanceof ManagedArray)) {
331            visitArray(value, propertyName, nestedBeanStack);
332        } else if (value instanceof List) {
333            visitList((List<?>) value, propertyName, nestedBeanStack);
334        } else if (value instanceof Set) {
335            visitSet((Set<?>) value, propertyName, nestedBeanStack);
336        } else if (value instanceof Map) {
337            visitMap((Map<?, ?>) value, propertyName, nestedBeanStack);
338        }
339    }
340
341    /**
342     * Iterates through the array values and calls helpers to process the value
343     *
344     * @param array the array to process
345     * @param propertyName name of the property which has the array value
346     * @param nestedBeanStack stack of bean containers which contains the array property
347     */
348    protected void visitArray(Object array, String propertyName, Stack<BeanDefinitionHolder> nestedBeanStack) {
349        Object[] arrayVal = null;
350        if (array instanceof ManagedArray) {
351            arrayVal = (Object[]) ((ManagedArray) array).getSource();
352        } else {
353            arrayVal = (Object[]) array;
354        }
355
356        Object[] newArray = new Object[arrayVal.length];
357        for (int i = 0; i < arrayVal.length; i++) {
358            Object elem = arrayVal[i];
359
360            if (isStringValue(elem)) {
361                elem = processArrayStringPropertyValue(propertyName, arrayVal, getString(elem), i, nestedBeanStack,
362                        beanProcessors);
363            } else {
364                elem = visitPropertyValue(propertyName, elem, nestedBeanStack);
365            }
366
367            newArray[i] = elem;
368        }
369
370        if (array instanceof ManagedArray) {
371            ((ManagedArray) array).setSource(newArray);
372        } else {
373            array = newArray;
374        }
375    }
376
377    /**
378     * Iterates through the list values and calls helpers to process the value
379     *
380     * @param listVal the list to process
381     * @param propertyName name of the property which has the list value
382     * @param nestedBeanStack stack of bean containers which contains the list property
383     */
384    protected void visitList(List<?> listVal, String propertyName, Stack<BeanDefinitionHolder> nestedBeanStack) {
385        boolean isMergeEnabled = false;
386        if (listVal instanceof ManagedList) {
387            isMergeEnabled = ((ManagedList) listVal).isMergeEnabled();
388        }
389
390        ManagedList newList = new ManagedList();
391        newList.setMergeEnabled(isMergeEnabled);
392
393        for (int i = 0; i < listVal.size(); i++) {
394            Object elem = listVal.get(i);
395
396            if (isStringValue(elem)) {
397                elem = processListStringPropertyValue(propertyName, listVal, getString(elem), i, nestedBeanStack,
398                        beanProcessors);
399            } else {
400                elem = visitPropertyValue(propertyName, elem, nestedBeanStack);
401            }
402
403            newList.add(i, elem);
404        }
405
406        listVal.clear();
407        listVal.addAll(newList);
408    }
409
410    /**
411     * Iterates through the set values and calls helpers to process the value
412     *
413     * @param setVal the set to process
414     * @param propertyName name of the property which has the set value
415     * @param nestedBeanStack stack of bean containers which contains the set property
416     */
417    protected void visitSet(Set setVal, String propertyName, Stack<BeanDefinitionHolder> nestedBeanStack) {
418        boolean isMergeEnabled = false;
419        if (setVal instanceof ManagedSet) {
420            isMergeEnabled = ((ManagedSet) setVal).isMergeEnabled();
421        }
422
423        ManagedSet newSet = new ManagedSet();
424        newSet.setMergeEnabled(isMergeEnabled);
425
426        for (Object elem : setVal) {
427            if (isStringValue(elem)) {
428                elem = processSetStringPropertyValue(propertyName, setVal, getString(elem), nestedBeanStack,
429                        beanProcessors);
430            } else {
431                elem = visitPropertyValue(propertyName, elem, nestedBeanStack);
432            }
433
434            newSet.add(elem);
435        }
436
437        setVal.clear();
438        setVal.addAll(newSet);
439    }
440
441    /**
442     * Iterates through the map values and calls helpers to process the value
443     *
444     * @param mapVal the set to process
445     * @param propertyName name of the property which has the map value
446     * @param nestedBeanStack stack of bean containers which contains the map property
447     */
448    protected void visitMap(Map<?, ?> mapVal, String propertyName, Stack<BeanDefinitionHolder> nestedBeanStack) {
449        boolean isMergeEnabled = false;
450        if (mapVal instanceof ManagedMap) {
451            isMergeEnabled = ((ManagedMap) mapVal).isMergeEnabled();
452        }
453
454        ManagedMap newMap = new ManagedMap();
455        newMap.setMergeEnabled(isMergeEnabled);
456
457        for (Map.Entry entry : mapVal.entrySet()) {
458            Object key = entry.getKey();
459            Object val = entry.getValue();
460
461            if (isStringValue(val)) {
462                val = processMapStringPropertyValue(propertyName, mapVal, getString(val), key, nestedBeanStack,
463                        beanProcessors);
464            } else {
465                val = visitPropertyValue(propertyName, val, nestedBeanStack);
466            }
467
468            newMap.put(key, val);
469        }
470
471        mapVal.clear();
472        mapVal.putAll(newMap);
473    }
474
475    /**
476     * Indicate whether the given value is a string or holds a string
477     *
478     * @param value value to test
479     * @return boolean true if the value is a string, false if not
480     */
481    protected boolean isStringValue(Object value) {
482        boolean isString = false;
483
484        if (value instanceof TypedStringValue || (value instanceof String)) {
485            isString = true;
486        }
487
488        return isString;
489    }
490
491    /**
492     * Determines whether the given value is of String type and if so returns the string value
493     *
494     * @param value object value to check
495     * @return String string value for object or null if object is not a string type
496     */
497    protected String getString(Object value) {
498        String stringValue = null;
499
500        if (value instanceof TypedStringValue || (value instanceof String)) {
501            if (value instanceof TypedStringValue) {
502                TypedStringValue typedStringValue = (TypedStringValue) value;
503                stringValue = typedStringValue.getValue();
504            } else {
505                stringValue = (String) value;
506            }
507        }
508
509        return stringValue;
510    }
511
512    /**
513     * Indicate whether the given value is a bean definition (or holder)
514     *
515     * @param value value to test
516     * @return boolean true if the value is a bean definition, false if not
517     */
518    protected boolean isBeanDefinitionValue(Object value) {
519        boolean isBean = false;
520
521        if ((value instanceof BeanDefinition) || (value instanceof BeanDefinitionHolder)) {
522            isBean = true;
523        }
524
525        return isBean;
526    }
527
528    /**
529     * Returns the given value as a bean definition (parsing from holder if necessary)
530     *
531     * @param value value to convert
532     * @return BeanDefinition converted bean definition
533     */
534    protected BeanDefinition getBeanDefinition(Object value) {
535        BeanDefinition beanDefinition = null;
536
537        if ((value instanceof BeanDefinition) || (value instanceof BeanDefinitionHolder)) {
538            if (value instanceof BeanDefinition) {
539                beanDefinition = (BeanDefinition) value;
540            } else {
541                beanDefinition = ((BeanDefinitionHolder) value).getBeanDefinition();
542            }
543        }
544
545        return beanDefinition;
546    }
547
548    /**
549     * Gets the bean name from the given value which is assumed to be a bean definition holder
550     *
551     * @param value value retrieve bean name from
552     * @return String bean name, or null if value is not a bean definition holder
553     */
554    protected String getBeanName(Object value) {
555        String beanName = null;
556
557        if (value instanceof BeanDefinitionHolder) {
558            beanName = ((BeanDefinitionHolder) value).getBeanName();
559        }
560
561        return beanName;
562    }
563
564    /**
565     * Indicate whether the given value is a collection
566     *
567     * @param value value to test
568     * @return boolean true if the value is a collection, false if not
569     */
570    protected boolean isCollectionValue(Object value) {
571        boolean isCollection = false;
572
573        if (value instanceof Object[] || (value instanceof ManagedArray) || (value instanceof List) ||
574                (value instanceof Set) || (value instanceof Map)) {
575            isCollection = true;
576        }
577
578        return isCollection;
579    }
580
581    /**
582     * Retrieves the data dictionary service using the KRAD service locator
583     *
584     * @return DataDictionaryService instance
585     */
586    protected DataDictionaryService getDataDictionaryService() {
587        return KRADServiceLocatorWeb.getDataDictionaryService();
588    }
589
590}