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