View Javadoc
1   /**
2    * Copyright 2005-2016 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.krad.uif.service.impl;
17  
18  import java.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.core.api.CoreApiServiceLocator;
28  import org.kuali.rice.core.api.config.property.ConfigurationService;
29  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
30  import org.kuali.rice.krad.service.LookupService;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.UifParameters;
33  import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
34  import org.kuali.rice.krad.uif.field.AttributeQuery;
35  import org.kuali.rice.krad.uif.field.AttributeQueryResult;
36  import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata;
37  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
38  import org.kuali.rice.krad.uif.service.AttributeQueryService;
39  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
40  import org.kuali.rice.krad.uif.widget.LocationSuggest;
41  import org.kuali.rice.krad.uif.widget.Suggest;
42  import org.kuali.rice.krad.util.BeanPropertyComparator;
43  
44  /**
45   * Implementation of <code>AttributeQueryService</code> that prepares the attribute queries and
46   * delegates to the <code>LookupService</code>
47   *
48   * @author Kuali Rice Team (rice.collab@kuali.org)
49   */
50  public class AttributeQueryServiceImpl implements AttributeQueryService {
51      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
52              AttributeQueryServiceImpl.class);
53  
54      private LookupService lookupService;
55      private ConfigurationService configurationService;
56  
57      /**
58       * {@inheritDoc}
59       */
60      @Override
61      public AttributeQueryResult performFieldSuggestQuery(ViewPostMetadata viewPostMetadata, String fieldId,
62              String fieldTerm, Map<String, String> queryParameters) {
63          AttributeQueryResult queryResult = new AttributeQueryResult();
64  
65          ComponentPostMetadata inputFieldMetaData = viewPostMetadata.getComponentPostMetadata(fieldId);
66          if (inputFieldMetaData == null) {
67              throw new RuntimeException("Unable to find attribute field instance for id: " + fieldId);
68          }
69  
70          Suggest.SuggestPostData suggestPostData = (Suggest.SuggestPostData) inputFieldMetaData.getData(
71                  UifConstants.PostMetadata.SUGGEST);
72  
73          AttributeQuery suggestQuery = suggestPostData.getSuggestQuery();
74  
75          boolean isUppercaseValue = Boolean.TRUE.equals(inputFieldMetaData.getData(
76                  UifConstants.PostMetadata.INPUT_FIELD_IS_UPPERCASE));
77  
78          // add term as a like criteria
79          Map<String, String> additionalCriteria = new HashMap<String, String>();
80          if (isUppercaseValue) {
81              additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm.toUpperCase() + "*");
82          } else {
83              additionalCriteria.put(suggestPostData.getValuePropertyName(), fieldTerm + "*");
84          }
85  
86          // execute suggest query
87          Collection<?> results = null;
88          if (suggestQuery.hasConfiguredMethod()) {
89              Object queryMethodResult = executeAttributeQueryMethod(suggestQuery, queryParameters, true, fieldTerm);
90              if ((queryMethodResult != null) && (queryMethodResult instanceof Collection<?>)) {
91                  results = (Collection<?>) queryMethodResult;
92              }
93          } else {
94              results = executeAttributeQueryCriteria(suggestQuery, queryParameters, additionalCriteria, new ArrayList<String>());
95          }
96  
97          // build list of suggest data from result records
98          if (results != null) {
99              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 }