View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.lookup;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.encryption.EncryptionService;
21  import org.kuali.rice.core.api.search.SearchOperator;
22  import org.kuali.rice.krad.bo.BusinessObject;
23  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
24  import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
25  import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
26  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
27  import org.kuali.rice.krad.service.ModuleService;
28  import org.kuali.rice.krad.util.BeanPropertyComparator;
29  import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
30  import org.kuali.rice.krad.util.KRADConstants;
31  import org.kuali.rice.krad.util.ObjectUtils;
32  import org.springframework.transaction.annotation.Transactional;
33  
34  import java.security.GeneralSecurityException;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.HashSet;
39  import java.util.Iterator;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Set;
43  
44  @Transactional
45  public class KualiLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
46  
47      protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiLookupableHelperServiceImpl.class);
48      protected boolean searchUsingOnlyPrimaryKeyValues = false;
49  
50  
51      /**
52       * Uses Lookup Service to provide a basic search.
53       *
54       * @param fieldValues - Map containing prop name keys and search values
55       *
56       * @return List found business objects
57       * @see LookupableHelperService#getSearchResults(java.util.Map)
58       */
59      public List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues) {
60          return getSearchResultsHelper(
61                  org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), false);
62      }
63  
64  
65      /**
66       * Uses Lookup Service to provide a basic unbounded search.
67       *
68       * @param fieldValues - Map containing prop name keys and search values
69       *
70       * @return List found business objects
71       * @see LookupableHelperService#getSearchResultsUnbounded(java.util.Map)
72       */
73      public List<? extends BusinessObject> getSearchResultsUnbounded(Map<String, String> fieldValues) {
74          return getSearchResultsHelper(
75                  org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), true);
76      }
77  
78      // TODO: Fix? - this does not handle nested properties within the EBO.
79  
80      /**
81       * Check whether the given property represents a property within an EBO starting
82       * with the sampleBo object given.  This is used to determine if a criteria needs
83       * to be applied to the EBO first, before sending to the normal lookup DAO.
84       */
85      protected boolean isExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
86          try {
87          	if ( propertyName.indexOf( "." ) > 0 && !StringUtils.contains( propertyName, "add." ) ) {
88  	        	Class propertyClass = PropertyUtils.getPropertyType(
89  						sampleBo, StringUtils.substringBeforeLast( propertyName, "." ) );
90  	        	if ( propertyClass != null ) {
91  	        		return ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface( propertyClass );
92  	        	} else {
93  	        		if ( LOG.isDebugEnabled() ) {
94  	        			LOG.debug( "unable to get class for " + StringUtils.substringBeforeLast( propertyName, "." ) + " on " + sampleBo.getClass().getName() );
95  	        		}
96  	        	}
97          	}
98          } catch (Exception e) {
99          	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 ( ExternalizableBusinessObjectUtils.isExternalizableBusinessObject( 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