View Javadoc

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