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.kns.lookup;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.search.SearchOperator;
021import org.kuali.rice.core.web.format.Formatter;
022import org.kuali.rice.kns.service.KNSServiceLocator;
023import org.kuali.rice.krad.bo.BusinessObject;
024import org.kuali.rice.krad.datadictionary.AttributeDefinition;
025import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
026import org.kuali.rice.krad.service.DataDictionaryService;
027import org.kuali.rice.krad.service.KRADServiceLocator;
028import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
029
030import java.lang.reflect.InvocationTargetException;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038/**
039 * LookupResults support strategy which uses the primary keys and lookupable defined in a business object's data dictionary file to support the multivalue lookup 
040 * 
041 * @author Kuali Rice Team (rice.collab@kuali.org)
042 *
043 */
044public class DataDictionaryLookupResultsSupportStrategy implements
045                LookupResultsSupportStrategyService {
046        
047        private DataDictionaryService dataDictionaryService;
048
049        /**
050         * Builds a lookup id for the given business object
051         * @see LookupResultsSupportStrategyService#getLookupIdForBusinessObject(org.kuali.rice.krad.bo.BusinessObject)
052         */
053        public String getLookupIdForBusinessObject(BusinessObject businessObject) {
054                final List<String> pkFieldNames = getPrimaryKeyFieldsForBusinessObject(businessObject.getClass());
055                return convertPKFieldMapToLookupId(pkFieldNames, businessObject);
056        }
057
058        /**
059         * Determines if both the primary keys and Lookupable are present in the data dictionary definition; if so, the BO qualifies, but if either are missing, it does not
060         * @see LookupResultsSupportStrategyService#qualifiesForStrategy(java.lang.Class)
061         */
062        public boolean qualifiesForStrategy(Class<? extends BusinessObject> boClass) {
063                if (getLookupableForBusinessObject(boClass) == null) {
064                        return false; // this probably isn't going to happen, but still...
065                }
066                final List<String> pkFields = getPrimaryKeyFieldsForBusinessObject(boClass);
067                return pkFields != null && pkFields.size() > 0;
068        }
069
070        /**
071         * Uses the Lookupable associated with the given BusinessObject to search for business objects
072         * @see LookupResultsSupportStrategyService#retrieveSelectedResultBOs(java.lang.String, java.lang.Class, java.lang.String, org.kuali.rice.krad.lookup.LookupResultsService)
073         */
074        public <T extends BusinessObject> Collection<T> retrieveSelectedResultBOs(Class<T> boClass, Set<String> lookupIds) throws Exception {
075        
076        List<T> retrievedBusinessObjects = new ArrayList<T>();
077        final org.kuali.rice.kns.lookup.Lookupable lookupable = getLookupableForBusinessObject(boClass);
078        for (String lookupId : lookupIds) {
079                final Map<String, String> lookupKeys = convertLookupIdToPKFieldMap(lookupId, boClass);
080                List<? extends BusinessObject> bos = lookupable.getSearchResults(lookupKeys);
081                
082                // we should only get one business object...but let's put them all in...
083                for (BusinessObject bo : bos) {
084                        retrievedBusinessObjects.add((T)bo);
085                }
086        }
087                
088                return retrievedBusinessObjects;
089        }
090        
091        /**
092         * Retrieves the Lookupable for the given business object class
093         * 
094         * @param businessObjectClass the class to find the Lookupable for
095         * @return the Lookupable, or null if nothing could be found
096         */
097        protected org.kuali.rice.kns.lookup.Lookupable getLookupableForBusinessObject(Class<? extends BusinessObject> businessObjectClass) {
098                final BusinessObjectEntry boEntry = getBusinessObjectEntry(businessObjectClass);
099                if (boEntry == null) {
100                        return null;
101                }
102                
103                final String lookupableId = boEntry.getLookupDefinition().getLookupableID();
104                return KNSServiceLocator.getLookupable(lookupableId);
105        }
106        
107        /**
108         * Returns the data dictionary defined primary keys for the given BusinessObject
109         * 
110         * @param businessObjectClass the business object to get DataDictionary defined primary keys for
111         * @return the List of primary key property names, or null if nothing could be found
112         */
113        protected List<String> getPrimaryKeyFieldsForBusinessObject(Class<? extends BusinessObject> businessObjectClass) {
114                final BusinessObjectEntry boEntry = getBusinessObjectEntry(businessObjectClass);
115                if (boEntry == null) {
116                        return null;
117                }
118                return boEntry.getPrimaryKeys();
119        }
120        
121        /**
122         * Converts a lookup id into a PK field map to use to search in a lookupable
123         * 
124         * @param lookupId the id returned by the lookup
125         * @param businessObjectClass the class of the business object getting the primary key
126         * @return a Map of field names and values which can be profitably used to search for matching business objects
127         */
128        protected Map<String, String> convertLookupIdToPKFieldMap(String lookupId, Class<? extends BusinessObject> businessObjectClass) {
129                Map<String, String> pkFields = new HashMap<String, String>();
130                if (!StringUtils.isBlank(lookupId)) {
131                        final String[] pkValues = lookupId.split("\\|");
132                        for (String pkValue : pkValues) {
133                                if (!StringUtils.isBlank(pkValue)) {
134                                        final String[] pkPieces = pkValue.split("-");
135                                        if (!StringUtils.isBlank(pkPieces[0]) && !StringUtils.isBlank(pkPieces[1])) {
136                                                pkFields.put(pkPieces[0], pkPieces[1]);
137                                        }
138                                }
139                        }
140                }
141                return pkFields;
142        }
143        
144        /**
145         * Converts a Map of PKFields into a String lookup ID
146         * @param pkFieldNames the name of the PK fields, which should be converted to the given lookupId
147         * @param businessObjectClass the class of the business object getting the primary key
148         * @return the String lookup id
149         */
150        protected String convertPKFieldMapToLookupId(List<String> pkFieldNames, BusinessObject businessObject) {
151                StringBuilder lookupId = new StringBuilder();
152                for (String pkFieldName : pkFieldNames) {
153                        try {
154                                final Object value = PropertyUtils.getProperty(businessObject, pkFieldName);
155                                
156                                if (value != null) {
157                                        lookupId.append(pkFieldName);
158                                        lookupId.append("-");
159                                        final Formatter formatter = retrieveBestFormatter(pkFieldName, businessObject.getClass());
160                                        final String formattedValue = (formatter != null) ? formatter.format(value).toString() : value.toString();
161                                        
162                                        lookupId.append(formattedValue);
163                                }
164                                lookupId.append(SearchOperator.OR.op());
165                        } catch (IllegalAccessException iae) {
166                                throw new RuntimeException("Could not retrieve pk field value "+pkFieldName+" from business object "+businessObject.getClass().getName(), iae);
167                        } catch (InvocationTargetException ite) {
168                                throw new RuntimeException("Could not retrieve pk field value "+pkFieldName+" from business object "+businessObject.getClass().getName(), ite);
169                        } catch (NoSuchMethodException nsme) {
170                                throw new RuntimeException("Could not retrieve pk field value "+pkFieldName+" from business object "+businessObject.getClass().getName(), nsme);
171                        }
172                }
173                return lookupId.substring(0, lookupId.length() - 1); // kill the last "|"
174        }
175        
176        /**
177         * Like when you're digging through your stuff drawer, you know the one in the kitchen with all the batteries and lint in it, this method
178         * goes through the stuff drawer of KNS formatters and attempts to return you a good one
179         * 
180         * @param propertyName the name of the property to retrieve
181         * @param boClass the class of the BusinessObject the property is on
182         * @return a Formatter, or null if we were unsuccessful in finding
183         */
184        protected Formatter retrieveBestFormatter(String propertyName, Class<? extends BusinessObject> boClass) {
185                Formatter formatter = null;
186                
187                try {
188                        Class<? extends Formatter> formatterClass = null;
189                        
190                        final BusinessObjectEntry boEntry = getBusinessObjectEntry(boClass);
191                        if (boEntry != null) {
192                                final AttributeDefinition attributeDefinition = boEntry.getAttributeDefinition(propertyName);
193                                if (attributeDefinition != null && attributeDefinition.hasFormatterClass()) {
194                                        formatterClass = (Class<? extends Formatter>)Class.forName(attributeDefinition.getFormatterClass());
195                                }
196                        }
197                        if (formatterClass == null) {
198                                final java.lang.reflect.Field propertyField = boClass.getDeclaredField(propertyName);
199                                if (propertyField != null) {
200                                        formatterClass = Formatter.findFormatter(propertyField.getType());
201                                }
202                        }
203                        
204                        if (formatterClass != null) {
205                                formatter = formatterClass.newInstance();
206                        }
207                } catch (SecurityException se) {
208                        throw new RuntimeException("Could not retrieve good formatter", se);
209                } catch (ClassNotFoundException cnfe) {
210                        throw new RuntimeException("Could not retrieve good formatter", cnfe);
211                } catch (NoSuchFieldException nsfe) {
212                        throw new RuntimeException("Could not retrieve good formatter", nsfe);
213                } catch (IllegalAccessException iae) {
214                        throw new RuntimeException("Could not retrieve good formatter", iae);
215                } catch (InstantiationException ie) {
216                        throw new RuntimeException("Could not retrieve good formatter", ie);
217                }
218                
219                return formatter;
220        }
221        
222        /**
223         * Looks up the DataDictionary BusinessObjectEntry for the given class
224         * 
225         * @param boClass the class of the BusinessObject to find a BusinessObjectEntry for
226         * @return the entry from the data dictionary, or null if nothing was found
227         */
228        protected BusinessObjectEntry getBusinessObjectEntry(Class<? extends BusinessObject> boClass) {
229                return (BusinessObjectEntry) getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(boClass.getName());
230        }
231
232        /**
233         * @return the dataDictionaryService
234         */
235        public DataDictionaryService getDataDictionaryService() {
236                return this.dataDictionaryService;
237        }
238
239        /**
240         * @param dataDictionaryService the dataDictionaryService to set
241         */
242        public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
243                this.dataDictionaryService = dataDictionaryService;
244        }
245}