001/** 002 * Copyright 2005-2015 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.krad.uif.service.impl; 017 018import java.text.MessageFormat; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.commons.lang.StringUtils; 027import org.kuali.rice.core.api.CoreApiServiceLocator; 028import org.kuali.rice.core.api.config.property.ConfigurationService; 029import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 030import org.kuali.rice.krad.service.LookupService; 031import org.kuali.rice.krad.uif.UifConstants; 032import org.kuali.rice.krad.uif.UifParameters; 033import org.kuali.rice.krad.uif.component.MethodInvokerConfig; 034import org.kuali.rice.krad.uif.field.AttributeQuery; 035import org.kuali.rice.krad.uif.field.AttributeQueryResult; 036import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata; 037import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata; 038import org.kuali.rice.krad.uif.service.AttributeQueryService; 039import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 040import org.kuali.rice.krad.uif.widget.LocationSuggest; 041import org.kuali.rice.krad.uif.widget.Suggest; 042import org.kuali.rice.krad.util.BeanPropertyComparator; 043 044/** 045 * Implementation of <code>AttributeQueryService</code> that prepares the attribute queries and 046 * delegates to the <code>LookupService</code> 047 * 048 * @author Kuali Rice Team (rice.collab@kuali.org) 049 */ 050public class AttributeQueryServiceImpl implements AttributeQueryService { 051 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( 052 AttributeQueryServiceImpl.class); 053 054 private LookupService lookupService; 055 private ConfigurationService configurationService; 056 057 /** 058 * {@inheritDoc} 059 */ 060 @Override 061 public AttributeQueryResult performFieldSuggestQuery(ViewPostMetadata viewPostMetadata, String fieldId, 062 String fieldTerm, Map<String, String> queryParameters) { 063 AttributeQueryResult queryResult = new AttributeQueryResult(); 064 065 ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId); 066 if (inputFieldMetaData == null) { 067 throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId); 068 } 069 070 Suggest.SuggestPostData suggestPostData = (Suggest.SuggestPostData) inputFieldMetaData.getData( 071 UifConstants.PostMetadata.SUGGEST); 072 073 AttributeQuery suggestQuery = suggestPostData.getSuggestQuery(); 074 075 boolean isUppercaseValue = Boolean.TRUE.equals(inputFieldMetaData.getData( 076 UifConstants.PostMetadata.INPUT_FIELD_IS_UPPERCASE)); 077 078 // add term as a like criteria 079 Map<String, String> additionalCriteria = new HashMap<String, String>(); 080 if (isUppercaseValue) { 081 additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm.toUpperCase() + "*"); 082 } else { 083 additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm + "*"); 084 } 085 086 // execute suggest query 087 Collection<?> results = null; 088 if (suggestQuery.hasConfiguredMethod()) { 089 Object queryMethodResult = executeAttributeQueryMethod(suggestQuery, queryParameters, true, fieldTerm); 090 if ((queryMethodResult != null) && (queryMethodResult instanceof Collection<?>)) { 091 results = (Collection<?>) queryMethodResult; 092 } 093 } else { 094 results = executeAttributeQueryCriteria(suggestQuery, queryParameters, additionalCriteria, new ArrayList<String>()); 095 } 096 097 // build list of suggest data from result records 098 if (results != null) { 099 if (suggestPostData.isReturnFullQueryObject()) { 100 queryResult.setResultData((List<Object>) results); 101 } else { 102 retrievePropertiesOnResults(queryResult, results, suggestPostData); 103 } 104 } 105 106 return queryResult; 107 } 108 109 /** 110 * Instead of returning the full object this method fills in queryResult with data that contain the properties 111 * of each result object, as configured through the fieldSuggest, from the set of results. 112 * 113 * @param queryResult the queryResult to fill in 114 * @param results the set of original results 115 * @param suggestPostData post data for the suggest widget 116 */ 117 protected void retrievePropertiesOnResults(AttributeQueryResult queryResult, Collection<?> results, 118 Suggest.SuggestPostData suggestPostData) { 119 List<Object> suggestData = new ArrayList<Object>(); 120 for (Object result : results) { 121 if (result == null) { 122 continue; 123 } 124 125 Map<String, String> propMap = new HashMap<String, String>(); 126 127 // if result is type string, use as both value and label 128 if (result instanceof String) { 129 propMap.put(UifParameters.VALUE, (String) result); 130 propMap.put(UifParameters.LABEL, (String) result); 131 } 132 133 // value prop 134 String suggestFieldValue = null; 135 if (StringUtils.isNotBlank(suggestPostData.getValuePropertyName())) { 136 suggestFieldValue = ObjectPropertyUtils.getPropertyValueAsText(result, 137 suggestPostData.getValuePropertyName()); 138 } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.VALUE)) { 139 suggestFieldValue = ObjectPropertyUtils.getPropertyValueAsText(result, UifParameters.VALUE); 140 } 141 142 if (suggestFieldValue != null) { 143 propMap.put(UifParameters.VALUE, suggestFieldValue); 144 } 145 146 // label prop 147 String suggestFieldLabel = null; 148 if (StringUtils.isNotBlank(suggestPostData.getLabelPropertyName())) { 149 suggestFieldLabel = ObjectPropertyUtils.getPropertyValueAsText(result, 150 suggestPostData.getLabelPropertyName()); 151 } else if (ObjectPropertyUtils.isReadableProperty(result, UifParameters.LABEL)) { 152 suggestFieldLabel = ObjectPropertyUtils.getPropertyValueAsText(result, UifParameters.LABEL); 153 } 154 155 if (suggestFieldLabel != null) { 156 propMap.put(UifParameters.LABEL, suggestFieldLabel); 157 } 158 159 // location suggest specific properties 160 if (suggestPostData instanceof LocationSuggest.LocationSuggestPostData) { 161 handleLocationSuggestProperties((LocationSuggest.LocationSuggestPostData) suggestPostData, result, 162 propMap); 163 } 164 165 // additional properties 166 handleAdditionalSuggestProperties(suggestPostData, result, propMap); 167 168 // only add if there was a property to send back 169 if (!propMap.isEmpty()) { 170 //TODO: need to apply formatter for field or have method in object property utils 171 suggestData.add(propMap); 172 } 173 } 174 175 queryResult.setResultData(suggestData); 176 } 177 178 /** 179 * Handle the custom additionalProperties set back for a suggestion query. These will be added to the propMap. 180 * 181 * @param suggestPostData post data for the suggest widget 182 * @param result the result to pull properties from 183 * @param propMap the propMap to add properties to 184 */ 185 private void handleAdditionalSuggestProperties(Suggest.SuggestPostData suggestPostData, Object result, 186 Map<String, String> propMap) { 187 if (suggestPostData.getAdditionalPropertiesToReturn() != null) { 188 //add properties for each valid property name 189 for (String propName : suggestPostData.getAdditionalPropertiesToReturn()) { 190 String propValue = null; 191 192 if (StringUtils.isNotBlank(propName) && ObjectPropertyUtils.isReadableProperty(result, propName)) { 193 propValue = ObjectPropertyUtils.getPropertyValueAsText(result, propName); 194 } 195 196 if (propValue != null) { 197 propMap.put(propName, propValue); 198 } 199 } 200 } 201 } 202 203 /** 204 * Handle the LocationSuggest specific properties and add them to the map. 205 * 206 * @param suggestPostData post data for the suggest widget 207 * @param result the result to pull properties from 208 * @param propMap the propMap to add properties to 209 */ 210 private void handleLocationSuggestProperties(LocationSuggest.LocationSuggestPostData suggestPostData, Object result, 211 Map<String, String> propMap) { 212 // href property 213 String suggestHrefValue = null; 214 if (StringUtils.isNotBlank(suggestPostData.getHrefPropertyName()) && ObjectPropertyUtils.isReadableProperty( 215 result, suggestPostData.getHrefPropertyName())) { 216 suggestHrefValue = ObjectPropertyUtils.getPropertyValueAsText(result, 217 suggestPostData.getHrefPropertyName()); 218 } 219 220 // add if found 221 if (suggestHrefValue != null) { 222 propMap.put(suggestPostData.getHrefPropertyName(), suggestHrefValue); 223 } 224 225 // url addition/appendage property 226 String addUrlValue = null; 227 if (StringUtils.isNotBlank(suggestPostData.getAdditionalUrlPathPropertyName()) && 228 ObjectPropertyUtils.isReadableProperty(result, suggestPostData.getAdditionalUrlPathPropertyName())) { 229 addUrlValue = ObjectPropertyUtils.getPropertyValueAsText(result, 230 suggestPostData.getAdditionalUrlPathPropertyName()); 231 } 232 233 // add if found 234 if (addUrlValue != null) { 235 propMap.put(suggestPostData.getAdditionalUrlPathPropertyName(), addUrlValue); 236 } 237 238 if (suggestPostData.getRequestParameterPropertyNames() == null) { 239 return; 240 } 241 242 // add properties for each valid requestParameter property name 243 for (String key : suggestPostData.getRequestParameterPropertyNames().keySet()) { 244 String prop = suggestPostData.getRequestParameterPropertyNames().get(key); 245 String propValue = null; 246 247 if (StringUtils.isNotBlank(prop) && ObjectPropertyUtils.isReadableProperty(result, prop)) { 248 propValue = ObjectPropertyUtils.getPropertyValueAsText(result, prop); 249 } 250 251 if (propValue != null) { 252 propMap.put(prop, propValue); 253 } 254 } 255 } 256 257 /** 258 * {@inheritDoc} 259 */ 260 @Override 261 public AttributeQueryResult performFieldQuery(ViewPostMetadata viewPostMetadata, String fieldId, 262 Map<String, String> queryParameters) { 263 AttributeQueryResult queryResult = new AttributeQueryResult(); 264 265 // retrieve attribute field from view index 266 ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId); 267 if (inputFieldMetaData == null) { 268 throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId); 269 } 270 271 AttributeQuery fieldQuery = (AttributeQuery) inputFieldMetaData 272 .getData(UifConstants.PostMetadata.INPUT_FIELD_ATTRIBUTE_QUERY); 273 if (fieldQuery == null) { 274 throw new RuntimeException("Field query not defined for field instance with id: " + fieldId); 275 } 276 277 // execute query and get result 278 Object resultObject = null; 279 if (fieldQuery.hasConfiguredMethod()) { 280 Object queryMethodResult = executeAttributeQueryMethod(fieldQuery, queryParameters, false, null); 281 if (queryMethodResult != null) { 282 // if method returned the result then no further processing needed 283 if (queryMethodResult instanceof AttributeQueryResult) { 284 return (AttributeQueryResult) queryMethodResult; 285 } 286 287 // if method returned collection, take first record 288 if (queryMethodResult instanceof Collection<?>) { 289 Collection<?> methodResultCollection = (Collection<?>) queryMethodResult; 290 if (!methodResultCollection.isEmpty()) { 291 resultObject = methodResultCollection.iterator().next(); 292 } 293 } else { 294 resultObject = queryMethodResult; 295 } 296 } 297 } else { 298 // execute field query as object lookup 299 Collection<?> results = executeAttributeQueryCriteria(fieldQuery, queryParameters, null, 300 new ArrayList<String>(queryParameters.keySet())); 301 302 if ((results != null) && !results.isEmpty()) { 303 // expect only one returned row for field query 304 if (results.size() > 1) { 305 //finding too many results in a not found message (not specific enough) 306 resultObject = null; 307 } else { 308 resultObject = results.iterator().next(); 309 } 310 } 311 } 312 313 if (resultObject != null) { 314 // build result field data map 315 Map<String, String> resultFieldData = new HashMap<String, String>(); 316 for (String fromField : fieldQuery.getReturnFieldMapping().keySet()) { 317 String returnField = fieldQuery.getReturnFieldMapping().get(fromField); 318 319 String fieldValueStr = ""; 320 fieldValueStr = ObjectPropertyUtils.getPropertyValueAsText(resultObject, fromField); 321 322 resultFieldData.put(returnField, fieldValueStr); 323 } 324 queryResult.setResultFieldData(resultFieldData); 325 326 fieldQuery.setReturnMessageText(""); 327 } else { 328 // add data not found message 329 if (fieldQuery.isRenderNotFoundMessage()) { 330 String messageTemplate = getConfigurationService().getPropertyValueAsString( 331 UifConstants.MessageKeys.QUERY_DATA_NOT_FOUND); 332 String message = MessageFormat.format(messageTemplate, inputFieldMetaData.getData( 333 UifConstants.PostMetadata.LABEL)); 334 fieldQuery.setReturnMessageText(message.toLowerCase()); 335 } 336 } 337 338 // set message and message style classes on query result 339 queryResult.setResultMessage(fieldQuery.getReturnMessageText()); 340 queryResult.setResultMessageStyleClasses(fieldQuery.getReturnMessageStyleClasses()); 341 342 return queryResult; 343 } 344 345 /** 346 * Prepares the method configured on the attribute query then performs the method invocation 347 * 348 * @param attributeQuery attribute query instance to execute 349 * @param queryParameters map of query parameters that provide values for the method arguments 350 * @param isSuggestQuery indicates whether the query is for forming suggest options 351 * @param queryTerm if being called for a suggest, the term for the query field 352 * @return type depends on method being invoked, could be AttributeQueryResult in which 353 * case the method has prepared the return result, or an Object that needs to be parsed for the result 354 */ 355 protected Object executeAttributeQueryMethod(AttributeQuery attributeQuery, Map<String, String> queryParameters, 356 boolean isSuggestQuery, String queryTerm) { 357 String queryMethodToCall = attributeQuery.getQueryMethodToCall(); 358 MethodInvokerConfig queryMethodInvoker = attributeQuery.getQueryMethodInvokerConfig(); 359 360 if (queryMethodInvoker == null) { 361 queryMethodInvoker = new MethodInvokerConfig(); 362 } 363 364 // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since 365 // there is not a getter), if so it will override the target method in prepare 366 if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) { 367 queryMethodInvoker.setTargetMethod(queryMethodToCall); 368 } 369 370 // setup query method arguments 371 List<Object> arguments = new ArrayList<Object>(); 372 if ((attributeQuery.getQueryMethodArgumentFieldList() != null) && 373 (!attributeQuery.getQueryMethodArgumentFieldList().isEmpty())) { 374 // retrieve argument types for conversion and verify method arguments 375 int numQueryMethodArguments = attributeQuery.getQueryMethodArgumentFieldList().size(); 376 if (isSuggestQuery) { 377 numQueryMethodArguments += 1; 378 } 379 380 // Empty arguments used to handle overloaded method case 381 queryMethodInvoker.setArguments(new Object[numQueryMethodArguments]); 382 Class<?>[] argumentTypes = queryMethodInvoker.getArgumentTypes(); 383 384 if ((argumentTypes == null) || (argumentTypes.length != numQueryMethodArguments)) { 385 throw new RuntimeException( 386 "Query method argument field list size does not match found number of method arguments"); 387 } 388 389 for (int i = 0; i < attributeQuery.getQueryMethodArgumentFieldList().size(); i++) { 390 String methodArgumentFromField = attributeQuery.getQueryMethodArgumentFieldList().get(i); 391 if (queryParameters.containsKey(methodArgumentFromField)) { 392 arguments.add(queryParameters.get(methodArgumentFromField)); 393 } else { 394 arguments.add(null); 395 } 396 } 397 } 398 399 if (isSuggestQuery) { 400 arguments.add(queryTerm); 401 } 402 403 queryMethodInvoker.setArguments(arguments.toArray()); 404 405 try { 406 queryMethodInvoker.prepare(); 407 408 return queryMethodInvoker.invoke(); 409 } catch (Exception e) { 410 throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e); 411 } 412 } 413 414 /** 415 * Prepares a query using the configured data object, parameters, and criteria, then executes 416 * the query and returns the result Collection 417 * 418 * @param attributeQuery attribute query instance to perform query for 419 * @param queryParameters map of parameters that will be used in the query criteria 420 * @param additionalCriteria map of additional name/value pairs to add to the critiera 421 * @param wildcardAsLiteralPropertyNames - List of property names with wildcards disabled 422 * @return results of query 423 */ 424 protected Collection<?> executeAttributeQueryCriteria(AttributeQuery attributeQuery, 425 Map<String, String> queryParameters, Map<String, String> additionalCriteria, 426 List<String> wildcardAsLiteralPropertyNames) { 427 // build criteria for query 428 boolean allQueryFieldsPresent = true; 429 430 Map<String, String> queryCriteria = new HashMap<String, String>(); 431 for (String fieldName : attributeQuery.getQueryFieldMapping().values()) { 432 if (queryParameters.containsKey(fieldName) && StringUtils.isNotBlank(queryParameters.get(fieldName))) { 433 queryCriteria.put(fieldName, queryParameters.get(fieldName)); 434 } else { 435 allQueryFieldsPresent = false; 436 break; 437 } 438 } 439 440 // for a field query we need all the criteria 441 if (!allQueryFieldsPresent) { 442 attributeQuery.setRenderNotFoundMessage(false); 443 444 return null; 445 } 446 447 // add any static criteria 448 for (String fieldName : attributeQuery.getAdditionalCriteria().keySet()) { 449 queryCriteria.put(fieldName, attributeQuery.getAdditionalCriteria().get(fieldName)); 450 } 451 452 // add additional criteria 453 if (additionalCriteria != null) { 454 queryCriteria.putAll(additionalCriteria); 455 } 456 457 Class<?> queryClass; 458 try { 459 queryClass = Class.forName(attributeQuery.getDataObjectClassName()); 460 } catch (ClassNotFoundException e) { 461 throw new RuntimeException( 462 "Invalid data object class given for suggest query: " + attributeQuery.getDataObjectClassName(), e); 463 } 464 465 // run query 466 Collection<?> results = getLookupService().findCollectionBySearchHelper(queryClass, queryCriteria, 467 wildcardAsLiteralPropertyNames, true, null); 468 469 // sort results 470 if (!attributeQuery.getSortPropertyNames().isEmpty() && (results != null) && (results.size() > 1)) { 471 Collections.sort((List<?>) results, new BeanPropertyComparator(attributeQuery.getSortPropertyNames())); 472 } 473 474 return results; 475 } 476 477 /** 478 * Gets the lookup service 479 * 480 * @return LookupService lookup service 481 */ 482 protected LookupService getLookupService() { 483 if (lookupService == null) { 484 lookupService = KRADServiceLocatorWeb.getLookupService(); 485 } 486 487 return lookupService; 488 } 489 490 /** 491 * Sets the lookup service 492 * 493 * @param lookupService 494 */ 495 public void setLookupService(LookupService lookupService) { 496 this.lookupService = lookupService; 497 } 498 499 /** 500 * Gets the configuration service 501 * 502 * @return configuration service 503 */ 504 protected ConfigurationService getConfigurationService() { 505 if (configurationService == null) { 506 configurationService = CoreApiServiceLocator.getKualiConfigurationService(); 507 } 508 509 return configurationService; 510 } 511 512 /** 513 * Sets the configuration service 514 * 515 * @param configurationService 516 */ 517 public void setConfigurationService(ConfigurationService configurationService) { 518 this.configurationService = configurationService; 519 } 520}