001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.kns.lookup;
017    
018    import org.apache.commons.beanutils.PropertyUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.kuali.rice.core.api.encryption.EncryptionService;
021    import org.kuali.rice.core.api.search.SearchOperator;
022    import org.kuali.rice.krad.bo.BusinessObject;
023    import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
024    import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
025    import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
026    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
027    import org.kuali.rice.krad.service.ModuleService;
028    import org.kuali.rice.krad.util.BeanPropertyComparator;
029    import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
030    import org.kuali.rice.krad.util.KRADConstants;
031    import org.kuali.rice.krad.util.ObjectUtils;
032    import org.springframework.transaction.annotation.Transactional;
033    
034    import java.security.GeneralSecurityException;
035    import java.util.ArrayList;
036    import java.util.Collections;
037    import java.util.HashMap;
038    import java.util.HashSet;
039    import java.util.Iterator;
040    import java.util.List;
041    import java.util.Map;
042    import java.util.Set;
043    
044    @Transactional
045    public class KualiLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
046    
047        protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiLookupableHelperServiceImpl.class);
048        protected boolean searchUsingOnlyPrimaryKeyValues = false;
049    
050    
051        /**
052         * Uses Lookup Service to provide a basic search.
053         *
054         * @param fieldValues - Map containing prop name keys and search values
055         *
056         * @return List found business objects
057         * @see LookupableHelperService#getSearchResults(java.util.Map)
058         */
059        public List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues) {
060            return getSearchResultsHelper(
061                    org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), false);
062        }
063    
064    
065        /**
066         * Uses Lookup Service to provide a basic unbounded search.
067         *
068         * @param fieldValues - Map containing prop name keys and search values
069         *
070         * @return List found business objects
071         * @see LookupableHelperService#getSearchResultsUnbounded(java.util.Map)
072         */
073        public List<? extends BusinessObject> getSearchResultsUnbounded(Map<String, String> fieldValues) {
074            return getSearchResultsHelper(
075                    org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), true);
076        }
077    
078        // TODO: Fix? - this does not handle nested properties within the EBO.
079    
080        /**
081         * Check whether the given property represents a property within an EBO starting
082         * with the sampleBo object given.  This is used to determine if a criteria needs
083         * to be applied to the EBO first, before sending to the normal lookup DAO.
084         */
085        protected boolean isExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
086            try {
087                    if ( propertyName.indexOf( "." ) > 0 && !StringUtils.contains( propertyName, "add." ) ) {
088                            Class propertyClass = PropertyUtils.getPropertyType(
089                                                    sampleBo, StringUtils.substringBeforeLast( propertyName, "." ) );
090                            if ( propertyClass != null ) {
091                                    return ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface( propertyClass );
092                            } else {
093                                    if ( LOG.isDebugEnabled() ) {
094                                            LOG.debug( "unable to get class for " + StringUtils.substringBeforeLast( propertyName, "." ) + " on " + sampleBo.getClass().getName() );
095                                    }
096                            }
097                    }
098            } catch (Exception e) {
099                    LOG.debug("Unable to determine type of property for " + sampleBo.getClass().getName() + "/" + propertyName, e );
100            }
101            return false;
102        }
103    
104        /**
105         * Get the name of the property which represents the ExternalizableBusinessObject for the given property.
106         *
107         * This method can not handle nested properties within the EBO.
108         *
109         * Returns null if the property is not a nested property or is part of an add line.
110         */
111        protected String getExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
112            if ( propertyName.indexOf( "." ) > 0 && !StringUtils.contains( propertyName, "add." ) ) {
113                    return StringUtils.substringBeforeLast( propertyName, "." );
114            }
115            return null;
116        }
117    
118        /**
119         * Checks whether any of the fieldValues being passed refer to a property within an ExternalizableBusinessObject.
120         */
121        protected boolean hasExternalBusinessObjectProperty(Class boClass, Map<String,String> fieldValues ) {
122            try {
123                    Object sampleBo = boClass.newInstance();
124                    for ( String key : fieldValues.keySet() ) {
125                            if ( isExternalBusinessObjectProperty( sampleBo, key )) {
126                                    return true;
127                            }
128                    }
129            } catch ( Exception ex ) {
130                    LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
131            }
132            return false;
133        }
134    
135        /**
136         * Returns a map stripped of any properties which refer to ExternalizableBusinessObjects.  These values may not be passed into the
137         * lookup service, since the objects they refer to are not in the local database.
138         */
139        protected Map<String,String> removeExternalizableBusinessObjectFieldValues(Class boClass, Map<String,String> fieldValues ) {
140            Map<String,String> eboFieldValues = new HashMap<String,String>();
141            try {
142                    Object sampleBo = boClass.newInstance();
143                    for ( String key : fieldValues.keySet() ) {
144                            if ( !isExternalBusinessObjectProperty( sampleBo, key )) {
145                                    eboFieldValues.put( key, fieldValues.get( key ) );
146                            }
147                    }
148            } catch ( Exception ex ) {
149                    LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
150            }
151            return eboFieldValues;
152        }
153    
154        /**
155         * Return the EBO fieldValue entries explicitly for the given eboPropertyName.  (I.e., any properties with the given
156         * property name as a prefix.
157         */
158        protected Map<String,String> getExternalizableBusinessObjectFieldValues(String eboPropertyName, Map<String,String> fieldValues ) {
159            Map<String,String> eboFieldValues = new HashMap<String,String>();
160            for ( String key : fieldValues.keySet() ) {
161                    if ( key.startsWith( eboPropertyName + "." ) ) {
162                            eboFieldValues.put( StringUtils.substringAfterLast( key, "." ), fieldValues.get( key ) );
163                    }
164            }
165            return eboFieldValues;
166        }
167    
168        /**
169         * Get the complete list of all properties referenced in the fieldValues that are ExternalizableBusinessObjects.
170         *
171         * This is a list of the EBO object references themselves, not of the properties within them.
172         */
173        protected List<String> getExternalizableBusinessObjectProperties(Class boClass, Map<String,String> fieldValues ) {
174            Set<String> eboPropertyNames = new HashSet<String>();
175            try {
176                    Object sampleBo = boClass.newInstance();
177                    for ( String key : fieldValues.keySet() ) {
178                            if ( isExternalBusinessObjectProperty( sampleBo, key )) {
179                                    eboPropertyNames.add( StringUtils.substringBeforeLast( key, "." ) );
180                            }
181                    }
182            } catch ( Exception ex ) {
183                    LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
184            }
185            return new ArrayList<String>(eboPropertyNames);
186        }
187    
188        /**
189         * Given an property on the main BO class, return the defined type of the ExternalizableBusinessObject.  This will be used
190         * by other code to determine the correct module service to call for the lookup.
191         *
192         * @param boClass
193         * @param propertyName
194         * @return
195         */
196        protected Class<? extends ExternalizableBusinessObject> getExternalizableBusinessObjectClass(Class boClass, String propertyName) {
197            try {
198                    return PropertyUtils.getPropertyType(
199                                            boClass.newInstance(), StringUtils.substringBeforeLast( propertyName, "." ) );
200            } catch (Exception e) {
201                    LOG.debug("Unable to determine type of property for " + boClass.getName() + "/" + propertyName, e );
202            }
203            return null;
204        }
205    
206        /**
207         *
208         * This method does the actual search, with the parameters specified, and returns the result.
209         *
210         * NOTE that it will not do any upper-casing based on the DD forceUppercase. That is handled through an external call to
211         * LookupUtils.forceUppercase().
212         *
213         * @param fieldValues A Map of the fieldNames and fieldValues to be searched on.
214         * @param unbounded Whether the results should be bounded or not to a certain max size.
215         * @return A List of search results.
216         *
217         */
218        protected List<? extends BusinessObject> getSearchResultsHelper(Map<String, String> fieldValues, boolean unbounded) {
219            // remove hidden fields
220            LookupUtils.removeHiddenCriteriaFields(getBusinessObjectClass(), fieldValues);
221    
222            searchUsingOnlyPrimaryKeyValues = getLookupService().allPrimaryKeyValuesPresentAndNotWildcard(getBusinessObjectClass(), fieldValues);
223    
224            setBackLocation(fieldValues.get(KRADConstants.BACK_LOCATION));
225            setDocFormKey(fieldValues.get(KRADConstants.DOC_FORM_KEY));
226            setReferencesToRefresh(fieldValues.get(KRADConstants.REFERENCES_TO_REFRESH));
227            List searchResults;
228            Map<String,String> nonBlankFieldValues = new HashMap<String, String>();
229            for (String fieldName : fieldValues.keySet()) {
230                    String fieldValue = fieldValues.get(fieldName);
231                    if (StringUtils.isNotBlank(fieldValue) ) {
232                            if (fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
233                                    String encryptedValue = StringUtils.removeEnd(fieldValue, EncryptionService.ENCRYPTION_POST_PREFIX);
234                                    try {
235                                            fieldValue = getEncryptionService().decrypt(encryptedValue);
236                                    }
237                                    catch (GeneralSecurityException e) {
238                                    LOG.error("Error decrypting value for business object " + getBusinessObjectService() + " attribute " + fieldName, e);
239                                    throw new RuntimeException("Error decrypting value for business object " + getBusinessObjectService() + " attribute " + fieldName, e);
240                            }
241                            }
242                            nonBlankFieldValues.put(fieldName, fieldValue);
243                    }
244            }
245    
246            // If this class is an EBO, just call the module service to get the results
247            if ( ExternalizableBusinessObject.class.isAssignableFrom( getBusinessObjectClass() ) ) {
248                    ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( getBusinessObjectClass() );
249                    BusinessObjectEntry ddEntry = eboModuleService.getExternalizableBusinessObjectDictionaryEntry(getBusinessObjectClass());
250                    Map<String,String> filteredFieldValues = new HashMap<String, String>();
251                    for (String fieldName : nonBlankFieldValues.keySet()) {
252                            if (ddEntry.getAttributeNames().contains(fieldName)) {
253                                    filteredFieldValues.put(fieldName, nonBlankFieldValues.get(fieldName));
254                            }
255                    }
256                    searchResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(getBusinessObjectClass(), (Map)filteredFieldValues, unbounded);
257            // if any of the properties refer to an embedded EBO, call the EBO lookups first and apply to the local lookup
258            } else if ( hasExternalBusinessObjectProperty( getBusinessObjectClass(), nonBlankFieldValues ) ) {
259                    if ( LOG.isDebugEnabled() ) {
260                            LOG.debug( "has EBO reference: " + getBusinessObjectClass() );
261                            LOG.debug( "properties: " + nonBlankFieldValues );
262                    }
263                    // remove the EBO criteria
264                    Map<String,String> nonEboFieldValues = removeExternalizableBusinessObjectFieldValues( getBusinessObjectClass(), nonBlankFieldValues );
265                    if ( LOG.isDebugEnabled() ) {
266                            LOG.debug( "Non EBO properties removed: " + nonEboFieldValues );
267                    }
268                    // get the list of EBO properties attached to this object
269                    List<String> eboPropertyNames = getExternalizableBusinessObjectProperties( getBusinessObjectClass(), nonBlankFieldValues );
270                    if ( LOG.isDebugEnabled() ) {
271                            LOG.debug( "EBO properties: " + eboPropertyNames );
272                    }
273                    // loop over those properties
274                    for ( String eboPropertyName : eboPropertyNames ) {
275                            // extract the properties as known to the EBO
276                            Map<String,String> eboFieldValues = getExternalizableBusinessObjectFieldValues( eboPropertyName, nonBlankFieldValues );
277                    if ( LOG.isDebugEnabled() ) {
278                            LOG.debug( "EBO properties for master EBO property: " + eboPropertyName );
279                            LOG.debug( "properties: " + eboFieldValues );
280                    }
281                    // run search against attached EBO's module service
282                    ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( getExternalizableBusinessObjectClass( getBusinessObjectClass(), eboPropertyName) );
283                    // KULRICE-4401 made eboResults an empty list and only filled if service is found.
284                            List eboResults = Collections.emptyList();
285                            if (eboModuleService != null) 
286                            {
287                                    eboResults = eboModuleService.getExternalizableBusinessObjectsListForLookup( getExternalizableBusinessObjectClass( getBusinessObjectClass(), eboPropertyName), (Map)eboFieldValues, unbounded);
288                            }
289                                    else
290                            {
291                                    LOG.debug( "EBO ModuleService is null: " + eboPropertyName );
292                            }
293                            // get the mapping/relationship between the EBO object and it's parent object
294                            // use that to adjust the fieldValues
295    
296                            // get the parent property type
297                            Class eboParentClass;
298                            String eboParentPropertyName;
299                            if ( ObjectUtils.isNestedAttribute( eboPropertyName ) ) {
300                                    eboParentPropertyName = StringUtils.substringBeforeLast( eboPropertyName, "." );
301                                    try {
302                                            eboParentClass = PropertyUtils.getPropertyType( getBusinessObjectClass().newInstance(), eboParentPropertyName );
303                                    } catch ( Exception ex ) {
304                                            throw new RuntimeException( "Unable to create an instance of the business object class: " + getBusinessObjectClass().getName(), ex );
305                                    }
306                            } else {
307                                    eboParentClass = getBusinessObjectClass();
308                                    eboParentPropertyName = null;
309                            }
310                            if ( LOG.isDebugEnabled() ) {
311                                    LOG.debug( "determined EBO parent class/property name: " + eboParentClass + "/" + eboParentPropertyName );
312                            }
313                            // look that up in the DD (BOMDS)
314                            // find the appropriate relationship
315                            // CHECK THIS: what if eboPropertyName is a nested attribute - need to strip off the eboParentPropertyName if not null
316                            RelationshipDefinition rd = getBusinessObjectMetaDataService().getBusinessObjectRelationshipDefinition( eboParentClass, eboPropertyName );
317                            if ( LOG.isDebugEnabled() ) {
318                                    LOG.debug( "Obtained RelationshipDefinition for " + eboPropertyName );
319                                    LOG.debug( rd );
320                            }
321    
322                            // copy the needed properties (primary only) to the field values
323                            // KULRICE-4446 do so only if the relationship definition exists
324                            // NOTE: this will work only for single-field PK unless the ORM layer is directly involved
325                            // (can't make (field1,field2) in ( (v1,v2),(v3,v4) ) style queries in the lookup framework
326                            if ( ObjectUtils.isNotNull(rd)) {
327                                    if ( rd.getPrimitiveAttributes().size() > 1 ) {
328                                            throw new RuntimeException( "EBO Links don't work for relationships with multiple-field primary keys." );
329                                    }
330                                    String boProperty = rd.getPrimitiveAttributes().get( 0 ).getSourceName();
331                                    String eboProperty = rd.getPrimitiveAttributes().get( 0 ).getTargetName();
332                                    StringBuffer boPropertyValue = new StringBuffer();
333                                    // loop over the results, making a string that the lookup DAO will convert into an
334                                    // SQL "IN" clause
335                                    for ( Object ebo : eboResults ) {
336                                            if ( boPropertyValue.length() != 0 ) {
337                                                    boPropertyValue.append( SearchOperator.OR.op() );
338                                            }
339                                            try {
340                                                    boPropertyValue.append( PropertyUtils.getProperty( ebo, eboProperty ).toString() );
341                                            } catch ( Exception ex ) {
342                                                    LOG.warn( "Unable to get value for " + eboProperty + " on " + ebo );
343                                            }
344                                    }
345                                    if ( eboParentPropertyName == null ) {
346                                            // non-nested property containing the EBO
347                                            nonEboFieldValues.put( boProperty, boPropertyValue.toString() );
348                                    } else {
349                                            // property nested within the main searched-for BO that contains the EBO
350                                            nonEboFieldValues.put( eboParentPropertyName + "." + boProperty, boPropertyValue.toString() );
351                                    }
352                            }
353                    }
354                    if ( LOG.isDebugEnabled() ) {
355                            LOG.debug( "Passing these results into the lookup service: " + nonEboFieldValues );
356                    }
357                    // add those results as criteria
358                    // run the normal search (but with the EBO critieria added)
359                    searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), nonEboFieldValues, unbounded);
360            } else {
361                searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), nonBlankFieldValues, unbounded);
362            }
363            
364            if (searchResults == null) {
365                    searchResults = new ArrayList();
366            }
367    
368            // sort list if default sort column given
369            List defaultSortColumns = getDefaultSortColumns();
370            if (defaultSortColumns.size() > 0) {
371                Collections.sort(searchResults, new BeanPropertyComparator(defaultSortColumns, true));
372            }
373            return searchResults;
374        }
375    
376    
377        /**
378         * @see LookupableHelperService#isSearchUsingOnlyPrimaryKeyValues()
379         */
380        @Override
381        public boolean isSearchUsingOnlyPrimaryKeyValues() {
382            return searchUsingOnlyPrimaryKeyValues;
383    }
384    
385    
386        /**
387         * Returns a comma delimited list of primary key field labels, to be used on the UI to tell the user which fields were used to search
388         *
389         * These labels are generated from the DD definitions for the lookup fields
390         *
391         * @return a comma separated list of field attribute names.  If no fields found, returns "N/A"
392         * @see LookupableHelperService#isSearchUsingOnlyPrimaryKeyValues()
393         * @see LookupableHelperService#getPrimaryKeyFieldLabels()
394         */
395        @Override
396        public String getPrimaryKeyFieldLabels() {
397            StringBuilder buf = new StringBuilder();
398            List<String> primaryKeyFieldNames = getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(getBusinessObjectClass());
399            Iterator<String> pkIter = primaryKeyFieldNames.iterator();
400            while (pkIter.hasNext()) {
401                String pkFieldName = (String) pkIter.next();
402                buf.append(getDataDictionaryService().getAttributeLabel(getBusinessObjectClass(), pkFieldName));
403                if (pkIter.hasNext()) {
404                    buf.append(", ");
405                }
406            }
407            return buf.length() == 0 ? KRADConstants.NOT_AVAILABLE_STRING : buf.toString();
408        }
409    
410    
411    }
412