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.krad.lookup;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.BooleanUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.kuali.rice.core.api.CoreApiServiceLocator;
22  import org.kuali.rice.core.api.config.property.ConfigurationService;
23  import org.kuali.rice.core.api.encryption.EncryptionService;
24  import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
25  import org.kuali.rice.core.api.search.SearchOperator;
26  import org.kuali.rice.core.api.util.RiceKeyConstants;
27  import org.kuali.rice.core.api.util.type.TypeUtils;
28  import org.kuali.rice.kim.api.identity.Person;
29  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
30  import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
31  import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
32  import org.kuali.rice.krad.service.DataObjectAuthorizationService;
33  import org.kuali.rice.krad.service.DataObjectMetaDataService;
34  import org.kuali.rice.krad.service.DocumentDictionaryService;
35  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
36  import org.kuali.rice.krad.service.LookupService;
37  import org.kuali.rice.krad.service.ModuleService;
38  import org.kuali.rice.krad.uif.UifConstants;
39  import org.kuali.rice.krad.uif.UifParameters;
40  import org.kuali.rice.krad.uif.UifPropertyPaths;
41  import org.kuali.rice.krad.uif.control.Control;
42  import org.kuali.rice.krad.uif.control.HiddenControl;
43  import org.kuali.rice.krad.uif.control.ValueConfiguredControl;
44  import org.kuali.rice.krad.uif.element.Link;
45  import org.kuali.rice.krad.uif.field.InputField;
46  import org.kuali.rice.krad.uif.field.LinkField;
47  import org.kuali.rice.krad.uif.field.LookupInputField;
48  import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
49  import org.kuali.rice.krad.uif.util.ComponentUtils;
50  import org.kuali.rice.krad.uif.util.LookupInquiryUtils;
51  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
52  import org.kuali.rice.krad.uif.view.History;
53  import org.kuali.rice.krad.uif.view.HistoryEntry;
54  import org.kuali.rice.krad.uif.view.LookupView;
55  import org.kuali.rice.krad.uif.view.View;
56  import org.kuali.rice.krad.util.BeanPropertyComparator;
57  import org.kuali.rice.krad.util.GlobalVariables;
58  import org.kuali.rice.krad.util.KRADConstants;
59  import org.kuali.rice.krad.util.KRADUtils;
60  import org.kuali.rice.krad.util.ObjectUtils;
61  import org.kuali.rice.krad.util.UrlFactory;
62  import org.kuali.rice.krad.web.form.LookupForm;
63  import org.kuali.rice.krad.web.form.UifFormBase;
64  
65  import java.security.GeneralSecurityException;
66  import java.util.ArrayList;
67  import java.util.Collection;
68  import java.util.Collections;
69  import java.util.HashMap;
70  import java.util.List;
71  import java.util.Map;
72  import java.util.Properties;
73  
74  /**
75   * Default implementation of <code>Lookupable</code>
76   *
77   * @author Kuali Rice Team (rice.collab@kuali.org)
78   */
79  public class LookupableImpl extends ViewHelperServiceImpl implements Lookupable {
80      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupableImpl.class);
81  
82      private Class<?> dataObjectClass;
83  
84      private transient ConfigurationService configurationService;
85      private transient DataObjectAuthorizationService dataObjectAuthorizationService;
86      private transient DataObjectMetaDataService dataObjectMetaDataService;
87      private transient DocumentDictionaryService documentDictionaryService;
88      private transient LookupService lookupService;
89      private transient EncryptionService encryptionService;
90  
91      /**
92       * Initialization of Lookupable requires that the business object class be set for the
93       * {@link #initializeDataFieldFromDataDictionary(org.kuali.rice.krad.uif.view.View,
94       * org.kuali.rice.krad.uif.field.DataField)} method
95       *
96       * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#performInitialization(org.kuali.rice.krad.uif.view.View,
97       *      java.lang.Object)
98       */
99      @Override
100     public void performInitialization(View view, Object model) {
101         if (!LookupView.class.isAssignableFrom(view.getClass())) {
102             throw new IllegalArgumentException(
103                     "View class '" + view.getClass() + " is not assignable from the '" + LookupView.class + "'");
104         }
105 
106         LookupView lookupView = (LookupView) view;
107         setDataObjectClass(lookupView.getDataObjectClassName());
108 
109         super.performInitialization(view, model);
110     }
111 
112     /**
113      * @see org.kuali.rice.krad.lookup.Lookupable#initSuppressAction(org.kuali.rice.krad.web.form.LookupForm)
114      */
115     @Override
116     public void initSuppressAction(LookupForm lookupForm) {
117         LookupViewAuthorizerBase lookupAuthorizer = (LookupViewAuthorizerBase) lookupForm.getView().getAuthorizer();
118         Person user = GlobalVariables.getUserSession().getPerson();
119         ((LookupView) lookupForm.getView()).setSuppressActions(!lookupAuthorizer.canInitiateDocument(lookupForm, user));
120     }
121 
122     /**
123      * @see org.kuali.rice.krad.lookup.Lookupable#performSearch
124      */
125     @Override
126     public Collection<?> performSearch(LookupForm form, Map<String, String> searchCriteria, boolean bounded) {
127         Collection<?> displayList;
128 
129         // TODO: force uppercase will be done in binding at some point
130         displayList = getSearchResults(form, LookupUtils.forceUppercase(getDataObjectClass(), searchCriteria),
131                 !bounded);
132 
133         // TODO delyea - is this the best way to set that the entire set has a returnable row?
134         for (Object object : displayList) {
135             if (isResultReturnable(object)) {
136                 form.setAtLeastOneRowReturnable(true);
137             }
138         }
139 
140         return displayList;
141     }
142 
143     /**
144      * Get the search results of the lookup
145      *
146      * @param form lookup form instance containing the lookup data
147      * @param searchCriteria map of criteria currently set
148      * @param unbounded indicates whether the complete result should be returned.  When set to false the result is
149      * limited (if necessary) to the max search result limit configured.
150      * @return the list of result objects, possibly bounded
151      */
152     protected List<?> getSearchResults(LookupForm form, Map<String, String> searchCriteria, boolean unbounded) {
153         List<?> searchResults;
154 
155         // removed blank search values and decrypt any encrypted search values
156         Map<String, String> nonBlankSearchCriteria = processSearchCriteria(form, searchCriteria);
157 
158         // return empty search results (none found) when the search doesn't have any nonBlankSearchCriteria although
159         // a filtered search criteria is specified
160         if (nonBlankSearchCriteria == null) {
161             return new ArrayList<Object>();
162         }
163 
164         // if this class is an EBO, just call the module service to get the results
165         if (ExternalizableBusinessObject.class.isAssignableFrom(getDataObjectClass())) {
166             return getSearchResultsForEBO(nonBlankSearchCriteria, unbounded);
167         }
168 
169         // if any of the properties refer to an embedded EBO, call the EBO
170         // lookups first and apply to the local lookup
171         try {
172             if (LookupUtils.hasExternalBusinessObjectProperty(getDataObjectClass(), nonBlankSearchCriteria)) {
173                 Map<String, String> eboSearchCriteria = adjustCriteriaForNestedEBOs(nonBlankSearchCriteria, unbounded);
174 
175                 if (LOG.isDebugEnabled()) {
176                     LOG.debug("Passing these results into the lookup service: " + eboSearchCriteria);
177                 }
178 
179                 // add those results as criteria run the normal search (but with the EBO criteria added)
180                 searchResults = (List<?>) getLookupService().findCollectionBySearchHelper(getDataObjectClass(),
181                         eboSearchCriteria, unbounded);
182             } else {
183                 searchResults = (List<?>) getLookupService().findCollectionBySearchHelper(getDataObjectClass(),
184                         nonBlankSearchCriteria, unbounded);
185             }
186         } catch (IllegalAccessException e) {
187             throw new RuntimeException("Error trying to perform search", e);
188         } catch (InstantiationException e1) {
189             throw new RuntimeException("Error trying to perform search", e1);
190         }
191 
192         if (searchResults == null) {
193             searchResults = new ArrayList<Object>();
194         } else {
195             sortSearchResults(form, searchResults);
196         }
197 
198         return searchResults;
199     }
200 
201     /**
202      * Sorts the given list of search results based on the lookup view's configured sort attributes
203      *
204      * <p>
205      * First if the posted view exists we grab the sort attributes from it. This will take into account expressions
206      * that might have been configured on the sort attributes. If the posted view does not exist (because we did a
207      * search from a get request or form session storage is off), we get the sort attributes from the view that we
208      * will be rendered (and was initialized before controller call). However, expressions will not be evaluated yet,
209      * thus if expressions were configured we don't know the results and can not sort the list
210      * </p>
211      *
212      * @param form - lookup form instance containing view information
213      * @param searchResults - list of search results to sort
214      * @TODO: revisit this when we have a solution for the posted view problem
215      */
216     protected void sortSearchResults(LookupForm form, List<?> searchResults) {
217         List<String> defaultSortColumns = null;
218 
219         // first choice is to get default sort columns off posted view, since that will include the full
220         // lifecycle and expression evaluations
221         if (form.getPostedView() != null) {
222             defaultSortColumns = ((LookupView) form.getPostedView()).getDefaultSortAttributeNames();
223         }
224         // now try view being built, if default sort attributes have any expression (entry is null) we can't use them
225         else if (form.getView() != null) {
226             defaultSortColumns = ((LookupView) form.getView()).getDefaultSortAttributeNames();
227 
228             boolean hasExpression = false;
229             if (defaultSortColumns != null) {
230                 for (String sortColumn : defaultSortColumns) {
231                     if (sortColumn == null) {
232                         hasExpression = true;
233                     }
234                 }
235             }
236 
237             if (hasExpression) {
238                 defaultSortColumns = null;
239             }
240         }
241 
242         if ((defaultSortColumns != null) && (defaultSortColumns.size() > 0)) {
243             Collections.sort(searchResults, new BeanPropertyComparator(defaultSortColumns, true));
244         }
245     }
246 
247     /**
248      * Process the search criteria to be used with the lookup
249      *
250      * <p>
251      * Processing entails primarily of the removal of filtered and unused/blank search criteria.  Encrypted field values
252      * are decrypted in this process as well.
253      * </p>
254      *
255      * @param lookupForm lookup form instance containing the lookup data
256      * @param searchCriteria map of criteria currently set
257      * @return map with the non blank search criteria
258      */
259     protected Map<String, String> processSearchCriteria(LookupForm lookupForm, Map<String, String> searchCriteria) {
260         Map<String, InputField> criteriaFields = new HashMap<String, InputField>();
261         if (lookupForm.getPostedView() != null) {
262             criteriaFields = getCriteriaFieldsForValidation((LookupView) lookupForm.getPostedView(), lookupForm);
263         }
264 
265         Map<String, String> filteredSearchCriteria = new HashMap<String,String>(searchCriteria);
266         for (String fieldName: searchCriteria.keySet()) {
267             InputField inputField = criteriaFields.get(fieldName);
268             if ((inputField == null) || !(inputField instanceof LookupInputField)) {
269                 continue;
270             }
271 
272             filteredSearchCriteria = ((LookupInputField) inputField).filterSearchCriteria(filteredSearchCriteria);
273             if (filteredSearchCriteria == null) {
274                 return null;
275             }
276         }
277 
278         Map<String, String> nonBlankSearchCriteria = new HashMap<String, String>();
279         for (String fieldName : filteredSearchCriteria.keySet()) {
280             String fieldValue = filteredSearchCriteria.get(fieldName);
281 
282             // don't add hidden criteria
283             InputField inputField = criteriaFields.get(fieldName);
284             if ((inputField != null) && (inputField.getControl() instanceof HiddenControl)) {
285                 continue;
286             }
287 
288             // only add criteria if non blank
289             if (StringUtils.isNotBlank(fieldValue)) {
290                 if (fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
291                     String encryptedValue = StringUtils.removeEnd(fieldValue, EncryptionService.ENCRYPTION_POST_PREFIX);
292                     try {
293                         if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
294                             fieldValue = getEncryptionService().decrypt(encryptedValue);
295                         }
296                     } catch (GeneralSecurityException e) {
297                         LOG.error("Error decrypting value for business object class " + getDataObjectClass() +
298                                 " attribute " + fieldName, e);
299                         throw new RuntimeException(
300                                 "Error decrypting value for business object class " + getDataObjectClass() +
301                                         " attribute " + fieldName, e);
302                     }
303                 }
304 
305                 nonBlankSearchCriteria.put(fieldName, fieldValue);
306             }
307         }
308 
309         return nonBlankSearchCriteria;
310     }
311 
312     /**
313      * Get the search results of an {@linkExternalizableBusinessObject}
314      *
315      * @param searchCriteria map of criteria currently set
316      * @param unbounded indicates whether the complete result should be returned.  When set to false the result is
317      * limited (if necessary) to the max search result limit configured.
318      * @return list of result objects, possibly bounded
319      */
320     protected List<?> getSearchResultsForEBO(Map<String, String> searchCriteria, boolean unbounded) {
321         ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
322                 getDataObjectClass());
323         BusinessObjectEntry ddEntry = eboModuleService.getExternalizableBusinessObjectDictionaryEntry(
324                 getDataObjectClass());
325 
326         Map<String, String> filteredFieldValues = new HashMap<String, String>();
327         for (String fieldName : searchCriteria.keySet()) {
328             if (ddEntry.getAttributeNames().contains(fieldName)) {
329                 filteredFieldValues.put(fieldName, searchCriteria.get(fieldName));
330             }
331         }
332 
333         List<?> searchResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
334                 (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), (Map) filteredFieldValues,
335                 unbounded);
336 
337         return searchResults;
338     }
339 
340     /**
341      * @param searchCriteria map of criteria currently set
342      * @param unbounded indicates whether the complete result should be returned.  When set to false the result is
343      * limited (if necessary) to the max search result limit configured.
344      * @return
345      * @throws InstantiationException
346      * @throws IllegalAccessException
347      */
348     protected Map<String, String> adjustCriteriaForNestedEBOs(Map<String, String> searchCriteria,
349             boolean unbounded) throws InstantiationException, IllegalAccessException {
350         if (LOG.isDebugEnabled()) {
351             LOG.debug("has EBO reference: " + getDataObjectClass());
352             LOG.debug("properties: " + searchCriteria);
353         }
354 
355         // remove the EBO criteria
356         Map<String, String> nonEboFieldValues = LookupUtils.removeExternalizableBusinessObjectFieldValues(
357                 getDataObjectClass(), searchCriteria);
358         if (LOG.isDebugEnabled()) {
359             LOG.debug("Non EBO properties removed: " + nonEboFieldValues);
360         }
361 
362         // get the list of EBO properties attached to this object
363         List<String> eboPropertyNames = LookupUtils.getExternalizableBusinessObjectProperties(getDataObjectClass(),
364                 searchCriteria);
365         if (LOG.isDebugEnabled()) {
366             LOG.debug("EBO properties: " + eboPropertyNames);
367         }
368 
369         // loop over those properties
370         for (String eboPropertyName : eboPropertyNames) {
371             // extract the properties as known to the EBO
372             Map<String, String> eboFieldValues = LookupUtils.getExternalizableBusinessObjectFieldValues(eboPropertyName,
373                     searchCriteria);
374             if (LOG.isDebugEnabled()) {
375                 LOG.debug("EBO properties for master EBO property: " + eboPropertyName);
376                 LOG.debug("properties: " + eboFieldValues);
377             }
378 
379             // run search against attached EBO's module service
380             ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
381                     LookupUtils.getExternalizableBusinessObjectClass(getDataObjectClass(), eboPropertyName));
382 
383             // KULRICE-4401 made eboResults an empty list and only filled if
384             // service is found.
385             List<?> eboResults = Collections.emptyList();
386             if (eboModuleService != null) {
387                 eboResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
388                         LookupUtils.getExternalizableBusinessObjectClass(getDataObjectClass(), eboPropertyName),
389                         (Map) eboFieldValues, unbounded);
390             } else {
391                 LOG.debug("EBO ModuleService is null: " + eboPropertyName);
392             }
393             // get the mapping/relationship between the EBO object and it's
394             // parent object
395             // use that to adjust the searchCriteria
396 
397             // get the parent property type
398             Class<?> eboParentClass;
399             String eboParentPropertyName;
400             if (ObjectUtils.isNestedAttribute(eboPropertyName)) {
401                 eboParentPropertyName = StringUtils.substringBeforeLast(eboPropertyName, ".");
402                 try {
403                     eboParentClass = PropertyUtils.getPropertyType(getDataObjectClass().newInstance(),
404                             eboParentPropertyName);
405                 } catch (Exception ex) {
406                     throw new RuntimeException(
407                             "Unable to create an instance of the business object class: " + getDataObjectClass()
408                                     .getName(), ex);
409                 }
410             } else {
411                 eboParentClass = getDataObjectClass();
412                 eboParentPropertyName = null;
413             }
414 
415             if (LOG.isDebugEnabled()) {
416                 LOG.debug("determined EBO parent class/property name: " + eboParentClass + "/" + eboParentPropertyName);
417             }
418 
419             // look that up in the DD (BOMDS)
420             // find the appropriate relationship
421             // CHECK THIS: what if eboPropertyName is a nested attribute -
422             // need to strip off the eboParentPropertyName if not null
423             RelationshipDefinition rd = getDataObjectMetaDataService().getDictionaryRelationship(eboParentClass,
424                     eboPropertyName);
425             if (LOG.isDebugEnabled()) {
426                 LOG.debug("Obtained RelationshipDefinition for " + eboPropertyName);
427                 LOG.debug(rd);
428             }
429 
430             // copy the needed properties (primary only) to the field values KULRICE-4446 do
431             // so only if the relationship definition exists
432             // NOTE: this will work only for single-field PK unless the ORM
433             // layer is directly involved
434             // (can't make (field1,field2) in ( (v1,v2),(v3,v4) ) style
435             // queries in the lookup framework
436             if (ObjectUtils.isNotNull(rd)) {
437                 if (rd.getPrimitiveAttributes().size() > 1) {
438                     throw new RuntimeException(
439                             "EBO Links don't work for relationships with multiple-field primary keys.");
440                 }
441                 String boProperty = rd.getPrimitiveAttributes().get(0).getSourceName();
442                 String eboProperty = rd.getPrimitiveAttributes().get(0).getTargetName();
443                 StringBuffer boPropertyValue = new StringBuffer();
444 
445                 // loop over the results, making a string that the lookup
446                 // DAO will convert into an
447                 // SQL "IN" clause
448                 for (Object ebo : eboResults) {
449                     if (boPropertyValue.length() != 0) {
450                         boPropertyValue.append(SearchOperator.OR.op());
451                     }
452                     try {
453                         boPropertyValue.append(PropertyUtils.getProperty(ebo, eboProperty).toString());
454                     } catch (Exception ex) {
455                         LOG.warn("Unable to get value for " + eboProperty + " on " + ebo);
456                     }
457                 }
458 
459                 if (eboParentPropertyName == null) {
460                     // non-nested property containing the EBO
461                     nonEboFieldValues.put(boProperty, boPropertyValue.toString());
462                 } else {
463                     // property nested within the main searched-for BO that
464                     // contains the EBO
465                     nonEboFieldValues.put(eboParentPropertyName + "." + boProperty, boPropertyValue.toString());
466                 }
467             }
468         }
469 
470         return nonEboFieldValues;
471     }
472 
473     /**
474      * @see org.kuali.rice.krad.lookup.Lookupable#performClear
475      */
476     @Override
477     public Map<String, String> performClear(LookupForm form, Map<String, String> searchCriteria) {
478         Map<String, InputField> criteriaFieldMap = new HashMap<String, InputField>();
479         if (form.getPostedView() == null) {
480             criteriaFieldMap = getCriteriaFieldsForValidation((LookupView) form.getPostedView(), form);
481         }
482 
483         Map<String, String> clearedSearchCriteria = new HashMap<String, String>();
484         for (Map.Entry<String, String> searchKeyValue : searchCriteria.entrySet()) {
485             String searchPropertyName = searchKeyValue.getKey();
486 
487             InputField inputField = criteriaFieldMap.get(searchPropertyName);
488             if (inputField != null) {
489                 // TODO: check secure fields
490                 //                                if (field.isSecure()) {
491                 //                    field.setSecure(false);
492                 //                    field.setDisplayMaskValue(null);
493                 //                    field.setEncryptedValue(null);
494                 //                }
495 
496                 // TODO: need formatting on default value and make sure it works when control converts
497                 // from checkbox to radio
498                 clearedSearchCriteria.put(searchPropertyName, inputField.getDefaultValue());
499             } else {
500                 clearedSearchCriteria.put(searchPropertyName, "");
501             }
502         }
503 
504         return clearedSearchCriteria;
505     }
506 
507     /**
508      * @see org.kuali.rice.krad.lookup.Lookupable#validateSearchParameters
509      */
510     @Override
511     public boolean validateSearchParameters(LookupForm form, Map<String, String> searchCriteria) {
512         boolean valid = true;
513 
514         // if postedView is null then we are executing the search from get request, in which case we
515         // can't validate the criteria
516         if (form.getPostedView() == null) {
517             return valid;
518         }
519 
520         Map<String, InputField> criteriaFields = getCriteriaFieldsForValidation((LookupView) form.getPostedView(),
521                 form);
522 
523         // build list of hidden properties configured with criteria fields
524         List<String> hiddenCriteria = new ArrayList<String>();
525         for (InputField field : criteriaFields.values()) {
526             if (field.getAdditionalHiddenPropertyNames() != null) {
527                 hiddenCriteria.addAll(field.getAdditionalHiddenPropertyNames());
528             }
529         }
530 
531         // validate required
532         // TODO: this will be done by the uif validation service at some point
533         for (Map.Entry<String, String> searchKeyValue : searchCriteria.entrySet()) {
534             String searchPropertyName = searchKeyValue.getKey();
535             String searchPropertyValue = searchKeyValue.getValue();
536 
537             InputField inputField = criteriaFields.get(searchPropertyName);
538 
539             String adjustedSearchPropertyPath = UifPropertyPaths.LOOKUP_CRITERIA + "[" + searchPropertyName + "]";
540             if (inputField == null && hiddenCriteria.contains(adjustedSearchPropertyPath)) {
541                 return valid;
542             }
543 
544 
545             // verify the property sent is a valid to search on
546             if ((inputField == null) && !searchPropertyName.contains(
547                     KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
548                 throw new RuntimeException("Invalid search field sent for property name: " + searchPropertyName);
549             }
550 
551             if (inputField != null) {
552                 if (StringUtils.isBlank(searchPropertyValue) && BooleanUtils.isTrue(inputField.getRequired())) {
553                     GlobalVariables.getMessageMap().putError(inputField.getPropertyName(), RiceKeyConstants.ERROR_REQUIRED,
554                             inputField.getLabel());
555                 }
556 
557                 validateSearchParameterWildcardAndOperators(inputField, searchPropertyValue);
558             }
559         }
560 
561         if (GlobalVariables.getMessageMap().hasErrors()) {
562             valid = false;
563         }
564 
565         return valid;
566     }
567 
568     /**
569      * Returns the criteria fields in a map keyed by the field property name.
570      *
571      * @param lookupView
572      * @param form lookup form instance containing the lookup data
573      * @return map of criteria fields
574      */
575     protected Map<String, InputField> getCriteriaFieldsForValidation(LookupView lookupView, LookupForm form) {
576         Map<String, InputField> criteriaFieldMap = new HashMap<String, InputField>();
577 
578         if (lookupView.getCriteriaFields() == null) {
579             return criteriaFieldMap;
580         }
581 
582         // TODO; need hooks for code generated components and also this doesn't have lifecycle which
583         // could change fields
584         List<InputField> fields = ComponentUtils.getComponentsOfTypeDeep(lookupView.getCriteriaFields(),
585                 InputField.class);
586         for (InputField field : fields) {
587             criteriaFieldMap.put(field.getPropertyName(), field);
588         }
589 
590         return criteriaFieldMap;
591     }
592 
593     /**
594      * Validates that any wildcards contained within the search value are valid wilcards and allowed for the
595      * property type for which the field is searching
596      *
597      * @param inputField - attribute field instance for the field that is being searched
598      * @param searchPropertyValue - value given for field to search for
599      */
600     protected void validateSearchParameterWildcardAndOperators(InputField inputField, String searchPropertyValue) {
601         if (StringUtils.isBlank(searchPropertyValue)) {
602             return;
603         }
604 
605         // make sure a wildcard/operator is in the value
606         boolean found = false;
607         for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
608             String queryCharacter = op.op();
609 
610             if (searchPropertyValue.contains(queryCharacter)) {
611                 found = true;
612             }
613         }
614 
615         if (!found) {
616             return;
617         }
618 
619         String attributeLabel = inputField.getLabel();
620         if ((LookupInputField.class.isAssignableFrom(inputField.getClass())) && (((LookupInputField) inputField)
621                 .isDisableWildcardsAndOperators())) {
622             Object dataObjectExample = null;
623             try {
624                 dataObjectExample = getDataObjectClass().newInstance();
625             } catch (Exception e) {
626                 LOG.error("Exception caught instantiating " + getDataObjectClass().getName(), e);
627                 throw new RuntimeException("Cannot instantiate " + getDataObjectClass().getName(), e);
628             }
629 
630             Class<?> propertyType = ObjectPropertyUtils.getPropertyType(getDataObjectClass(),
631                     inputField.getPropertyName());
632             if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) ||
633                     TypeUtils.isTemporalClass(propertyType)) {
634                 GlobalVariables.getMessageMap().putError(inputField.getPropertyName(),
635                         RiceKeyConstants.ERROR_WILDCARDS_AND_OPERATORS_NOT_ALLOWED_ON_FIELD, attributeLabel);
636             }
637 
638             if (TypeUtils.isStringClass(propertyType)) {
639                 GlobalVariables.getMessageMap().putInfo(inputField.getPropertyName(),
640                         RiceKeyConstants.INFO_WILDCARDS_AND_OPERATORS_TREATED_LITERALLY, attributeLabel);
641             }
642         } else {
643             if (getDataObjectAuthorizationService().attributeValueNeedsToBeEncryptedOnFormsAndLinks(
644                     getDataObjectClass(), inputField.getPropertyName())) {
645                 if (!searchPropertyValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
646                     // encrypted values usually come from the DB, so we don't
647                     // need to filter for wildcards
648                     // wildcards are not allowed on restricted fields, because
649                     // they are typically encrypted, and wildcard searches cannot be performed without
650                     // decrypting every row, which is currently not supported by KRAD
651 
652                     GlobalVariables.getMessageMap().putError(inputField.getPropertyName(),
653                             RiceKeyConstants.ERROR_SECURE_FIELD, attributeLabel);
654                 }
655             }
656         }
657     }
658 
659     /**
660      * @see org.kuali.rice.krad.lookup.Lookupable#getReturnUrlForResults
661      */
662     public void getReturnUrlForResults(LinkField returnLinkField, Object model) {
663         LookupForm lookupForm = (LookupForm) model;
664         LookupView lookupView = (LookupView) returnLinkField.getContext().get(UifConstants.ContextVariableNames.VIEW);
665 
666         Object dataObject = returnLinkField.getContext().get(UifConstants.ContextVariableNames.LINE);
667 
668         // don't render return link if the object is null or if the row is not returnable
669         if ((dataObject == null) || (!isResultReturnable(dataObject))) {
670             returnLinkField.setRender(false);
671             return;
672         }
673 
674         // build return link href
675         String href = getReturnUrl(lookupView, lookupForm, dataObject);
676         if (StringUtils.isBlank(href)) {
677             returnLinkField.setRender(false);
678             return;
679         }
680 
681         //special handling to handle history correctly
682         List<HistoryEntry> historyEntries = null;
683         History history = lookupForm.getFormHistory();
684 
685         if(history != null){
686             historyEntries = history.getGeneratedBreadcrumbs();
687         }
688 
689         String historyParams = "";
690         if(historyEntries != null && !historyEntries.isEmpty()){
691             //get the last entry
692             String url = historyEntries.get(historyEntries.size() - 1).getUrl();
693             if(url != null && url.indexOf('?') > -1 && (url.indexOf('?') + 1) < url.length()){
694                 historyParams = url.substring(url.indexOf('?') + 1);
695                 //remove method to call and form key; href string already has these
696                 historyParams = historyParams.replaceFirst("(^|&)" +
697                         KRADConstants.DISPATCH_REQUEST_PARAMETER + "=.*?($|&)","");
698                 historyParams = historyParams.replaceFirst("(^|&)" +
699                                     KRADConstants.FORM_KEY + "=.*?($|&)","");
700             }
701         }
702 
703         //append the modified history params
704         if(StringUtils.isNotBlank(historyParams)){
705             href = href + "&" + historyParams;
706         }
707 
708         //set the return link
709         returnLinkField.setHref(href);
710 
711         // build return link label and title
712         String linkLabel = getConfigurationService().getPropertyValueAsString(
713                 KRADConstants.Lookup.TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
714         returnLinkField.setLinkText(linkLabel);
715 
716         List<String> returnKeys = getReturnKeys(lookupView, lookupForm, dataObject);
717         Map<String, String> returnKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, dataObject);
718 
719         String title = LookupInquiryUtils.getLinkTitleText(linkLabel, getDataObjectClass(), returnKeyValues);
720         returnLinkField.setTitle(title);
721 
722         // Add the return target if it is set
723         String returnTarget = lookupView.getReturnTarget();
724         if (returnTarget != null) {
725             returnLinkField.setTarget(returnTarget);
726 
727             //  Add the close script if lookup is in a light box
728             if (!returnTarget.equals("_self")) {
729                 // Add the return script if the returnByScript flag is set
730                 if (lookupView.isReturnByScript()) {
731                     Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
732 
733                     StringBuilder script = new StringBuilder("e.preventDefault();");
734                     for (String returnField : lookupForm.getFieldConversions().values()) {
735                         if (props.containsKey(returnField)) {
736                             Object fieldName = returnField.replace("'", "\\'");
737                             Object value = props.get(returnField);
738                             script = script.append(
739                                     "returnLookupResultByScript(\"" + returnField + "\", '" + value + "');");
740                         }
741                     }
742                     returnLinkField.getLink().setOnClickScript(script.append("closeLightbox();").toString());
743                 } else {
744                     // Close the light box if return target is not _self or _parent
745                     returnLinkField.getLink().setOnClickScript(
746                             "e.preventDefault();closeLightbox();showLoading();" +
747                             "returnLookupResultReload(jQuery(this));");
748                 }
749             }
750         } else {
751             // If no return target is set return in same frame
752             // This is to insure that non light box lookups return correctly
753             returnLinkField.setTarget("_self");
754         }
755     }
756 
757     /**
758      * Builds the URL for returning the given data object result row
759      *
760      * <p>
761      * Note return URL will only be built if a return location is specified on the <code>LookupForm</code>
762      * </p>
763      *
764      * @param lookupView - lookup view instance containing lookup configuration
765      * @param lookupForm - lookup form instance containing the data
766      * @param dataObject - data object instance for the current line and for which the return URL is being built
767      * @return String return URL or blank if URL cannot be built
768      */
769     protected String getReturnUrl(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
770         Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
771 
772         String href = "";
773         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
774             href = UrlFactory.parameterizeUrl(lookupForm.getReturnLocation(), props);
775         }
776 
777         return href;
778     }
779 
780     /**
781      * Builds up a <code>Properties</code> object that will be used to provide the request parameters for the
782      * return URL link
783      *
784      * @param lookupView - lookup view instance containing lookup configuration
785      * @param lookupForm - lookup form instance containing the data
786      * @param dataObject - data object instance for the current line and for which the return URL is being built
787      * @return Properties instance containing request parameters for return URL
788      */
789     protected Properties getReturnUrlParameters(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
790         Properties props = new Properties();
791         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, KRADConstants.RETURN_METHOD_TO_CALL);
792 
793         if (StringUtils.isNotBlank(lookupForm.getReturnFormKey())) {
794             props.put(UifParameters.FORM_KEY, lookupForm.getReturnFormKey());
795         }
796 
797         props.put(KRADConstants.REFRESH_CALLER, lookupView.getId());
798         props.put(KRADConstants.REFRESH_DATA_OBJECT_CLASS, getDataObjectClass().getName());
799 
800         if (StringUtils.isNotBlank(lookupForm.getDocNum())) {
801             props.put(UifParameters.DOC_NUM, lookupForm.getDocNum());
802         }
803 
804         if (StringUtils.isNotBlank(lookupForm.getReferencesToRefresh())) {
805             props.put(KRADConstants.REFERENCES_TO_REFRESH, lookupForm.getReferencesToRefresh());
806         }
807 
808         List<String> returnKeys = getReturnKeys(lookupView, lookupForm, dataObject);
809         Map<String, String> returnKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, dataObject);
810 
811         for (String returnKey : returnKeyValues.keySet()) {
812             String returnValue = returnKeyValues.get(returnKey);
813             if (lookupForm.getFieldConversions().containsKey(returnKey)) {
814                 returnKey = lookupForm.getFieldConversions().get(returnKey);
815             }
816 
817             props.put(returnKey, returnValue);
818         }
819        // props.put(UifParameters.AJAX_REQUEST,"false");
820         return props;
821     }
822 
823     /**
824      * <p>Returns the configured return key property names or if not configured defaults to the primary keys
825      * for the data object class
826      * </p>
827      *
828      * @param lookupView - lookup view instance containing lookup configuration
829      * @param lookupForm - lookup form instance containing the data
830      * @param dataObject - data object instance
831      * @return List<String> property names which should be passed back on the return URL
832      */
833     protected List<String> getReturnKeys(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
834         List<String> returnKeys;
835         if (lookupForm.getFieldConversions() != null && !lookupForm.getFieldConversions().isEmpty()) {
836             returnKeys = new ArrayList<String>(lookupForm.getFieldConversions().keySet());
837         } else {
838             returnKeys = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
839         }
840 
841         return returnKeys;
842     }
843 
844     /**
845      * @see org.kuali.rice.krad.lookup.Lookupable#getMaintenanceActionLink
846      */
847     public void getMaintenanceActionLink(Link actionLink, Object model, String maintenanceMethodToCall) {
848         LookupForm lookupForm = (LookupForm) model;
849         LookupView lookupView = (LookupView) actionLink.getContext().get(UifConstants.ContextVariableNames.VIEW);
850         Object dataObject = actionLink.getContext().get(UifConstants.ContextVariableNames.LINE);
851 
852         List<String> pkNames = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
853 
854         // build maintenance link href
855         String href = getActionUrlHref(lookupForm, dataObject, maintenanceMethodToCall, pkNames);
856         if (StringUtils.isBlank(href)) {
857             actionLink.setRender(false);
858             return;
859         }
860         // TODO: need to handle returning anchor
861         actionLink.setHref(href);
862 
863         // build action title
864         String prependTitleText = actionLink.getLinkText() + " " +
865                 getDataDictionaryService().getDataDictionary().getDataObjectEntry(getDataObjectClass().getName())
866                         .getObjectLabel() + " " +
867                 getConfigurationService().getPropertyValueAsString(
868                         KRADConstants.Lookup.TITLE_ACTION_URL_PREPENDTEXT_PROPERTY);
869 
870         Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
871         String title = LookupInquiryUtils.getLinkTitleText(prependTitleText, getDataObjectClass(), primaryKeyValues);
872         actionLink.setTitle(title);
873         // TODO : do not hardcode the _self string
874         actionLink.setTarget("_self");
875         lookupForm.setAtLeastOneRowHasActions(true);
876     }
877 
878     /**
879      * Generates a URL to perform a maintenance action on the given result data object
880      *
881      * <p>
882      * Will build a URL containing keys of the data object to invoke the given maintenance action method
883      * within the maintenance controller
884      * </p>
885      *
886      * @param dataObject - data object instance for the line to build the maintenance action link for
887      * @param methodToCall - method name on the maintenance controller that should be invoked
888      * @param pkNames - list of primary key field names for the data object whose key/value pairs will be added to
889      * the maintenance link
890      * @return String URL link for the maintenance action
891      */
892     protected String getActionUrlHref(LookupForm lookupForm, Object dataObject, String methodToCall,
893             List<String> pkNames) {
894         LookupView lookupView = (LookupView) lookupForm.getView();
895 
896         Properties props = new Properties();
897         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
898 
899         Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
900         for (String primaryKey : primaryKeyValues.keySet()) {
901             String primaryKeyValue = primaryKeyValues.get(primaryKey);
902 
903             props.put(primaryKey, primaryKeyValue);
904         }
905 
906         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
907             props.put(KRADConstants.RETURN_LOCATION_PARAMETER, lookupForm.getReturnLocation());
908         }
909 
910         props.put(UifParameters.DATA_OBJECT_CLASS_NAME, lookupForm.getDataObjectClassName());
911         props.put(UifParameters.VIEW_TYPE_NAME, UifConstants.ViewType.MAINTENANCE.name());
912 
913         String maintenanceMapping = KRADConstants.Maintenance.REQUEST_MAPPING_MAINTENANCE;
914         if (StringUtils.isNotBlank(lookupView.getMaintenanceUrlMapping())) {
915             maintenanceMapping = lookupView.getMaintenanceUrlMapping();
916         }
917 
918         return UrlFactory.parameterizeUrl(maintenanceMapping, props);
919     }
920 
921     /**
922      * Sets the value for the attribute field control to contain the field conversion values for the line
923      *
924      * @see org.kuali.rice.krad.lookup.LookupableImpl#setMultiValueLookupSelect
925      */
926     @Override
927     public void setMultiValueLookupSelect(InputField selectField, Object model) {
928         LookupForm lookupForm = (LookupForm) model;
929         Object lineDataObject = selectField.getContext().get(UifConstants.ContextVariableNames.LINE);
930         if (lineDataObject == null) {
931             throw new RuntimeException("Unable to get data object for line from component: " + selectField.getId());
932         }
933 
934         Control selectControl = ((InputField) selectField).getControl();
935         if ((selectControl != null) && (selectControl instanceof ValueConfiguredControl)) {
936             String lineIdentifier = "";
937 
938             // get value for each field conversion from line and add to lineIdentifier
939             Map<String, String> fieldConversions = lookupForm.getFieldConversions();
940             List<String> fromFieldNames = new ArrayList<String>(fieldConversions.keySet());
941             Collections.sort(fromFieldNames);
942             for (String fromFieldName : fromFieldNames) {
943                 Object fromFieldValue = ObjectPropertyUtils.getPropertyValue(lineDataObject, fromFieldName);
944                 if (fromFieldValue != null) {
945                     lineIdentifier += fromFieldValue;
946                 }
947                 lineIdentifier += ":";
948             }
949             lineIdentifier = StringUtils.removeEnd(lineIdentifier, ":");
950 
951             ((ValueConfiguredControl) selectControl).setValue(lineIdentifier);
952         }
953     }
954 
955     /**
956      * Determines if given data object has associated maintenance document that allows new or copy
957      * maintenance
958      * actions
959      *
960      * @return boolean true if the maintenance new or copy action is allowed for the data object instance, false
961      *         otherwise
962      */
963     public boolean allowsMaintenanceNewOrCopyAction() {
964         boolean allowsNewOrCopy = false;
965 
966         String maintDocTypeName = getMaintenanceDocumentTypeName();
967         if (StringUtils.isNotBlank(maintDocTypeName)) {
968             allowsNewOrCopy = getDataObjectAuthorizationService().canCreate(getDataObjectClass(),
969                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
970         }
971 
972         return allowsNewOrCopy;
973     }
974 
975     /**
976      * Determines if given data object has associated maintenance document that allows edit maintenance
977      * actions
978      *
979      * @return boolean true if the maintenance edit action is allowed for the data object instance, false otherwise
980      */
981     public boolean allowsMaintenanceEditAction(Object dataObject) {
982         boolean allowsEdit = false;
983 
984         String maintDocTypeName = getMaintenanceDocumentTypeName();
985         if (StringUtils.isNotBlank(maintDocTypeName)) {
986             allowsEdit = getDataObjectAuthorizationService().canMaintain(dataObject,
987                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
988         }
989 
990         return allowsEdit;
991     }
992 
993     /**
994      * Determines if given data object has associated maintenance document that allows delete maintenance
995      * actions.
996      *
997      * @return boolean true if the maintenance delete action is allowed for the data object instance, false otherwise
998      */
999     public boolean allowsMaintenanceDeleteAction(Object dataObject) {
1000         boolean allowsMaintain = false;
1001         boolean allowsDelete = false;
1002 
1003         String maintDocTypeName = getMaintenanceDocumentTypeName();
1004         if (StringUtils.isNotBlank(maintDocTypeName)) {
1005             allowsMaintain = getDataObjectAuthorizationService().canMaintain(dataObject,
1006                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
1007         }
1008 
1009         allowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion(getDataObjectClass());
1010 
1011         return allowsDelete && allowsMaintain;
1012     }
1013 
1014     /**
1015      * Returns the maintenance document type associated with the business object class or null if one does not exist.
1016      *
1017      * @return String representing the maintenance document type name
1018      */
1019     protected String getMaintenanceDocumentTypeName() {
1020         DocumentDictionaryService dd = getDocumentDictionaryService();
1021         String maintDocTypeName = dd.getMaintenanceDocumentTypeName(getDataObjectClass());
1022 
1023         return maintDocTypeName;
1024     }
1025 
1026     /**
1027      * Determines whether a given data object that's returned as one of the lookup's results is considered returnable,
1028      * which means that for single-value lookups, a "return value" link may be rendered, and for multiple
1029      * value lookups, a checkbox is rendered.
1030      *
1031      * Note that this can be part of an authorization mechanism, but not the complete authorization mechanism.  The
1032      * component that invoked the lookup/ lookup caller (e.g. document, nesting lookup, etc.) needs to check
1033      * that the object that was passed to it was returnable as well because there are ways around this method
1034      * (e.g. crafting a custom return URL).
1035      *
1036      * @param dataObject - an object from the search result set
1037      * @return true if the row is returnable and false if it is not
1038      */
1039     protected boolean isResultReturnable(Object dataObject) {
1040         return true;
1041     }
1042 
1043     /**
1044      * @see org.kuali.rice.krad.lookup.Lookupable#setDataObjectClass
1045      */
1046     @Override
1047     public void setDataObjectClass(Class<?> dataObjectClass) {
1048         this.dataObjectClass = dataObjectClass;
1049     }
1050 
1051     /**
1052      * @see org.kuali.rice.krad.lookup.Lookupable#getDataObjectClass
1053      */
1054     @Override
1055     public Class<?> getDataObjectClass() {
1056         return this.dataObjectClass;
1057     }
1058 
1059     public void setConfigurationService(ConfigurationService configurationService) {
1060         this.configurationService = configurationService;
1061     }
1062 
1063     protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
1064         if (dataObjectAuthorizationService == null) {
1065             this.dataObjectAuthorizationService = KRADServiceLocatorWeb.getDataObjectAuthorizationService();
1066         }
1067         return dataObjectAuthorizationService;
1068     }
1069 
1070     public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
1071         this.dataObjectAuthorizationService = dataObjectAuthorizationService;
1072     }
1073 
1074     protected DataObjectMetaDataService getDataObjectMetaDataService() {
1075         if (dataObjectMetaDataService == null) {
1076             this.dataObjectMetaDataService = KRADServiceLocatorWeb.getDataObjectMetaDataService();
1077         }
1078         return dataObjectMetaDataService;
1079     }
1080 
1081     public void setDataObjectMetaDataService(DataObjectMetaDataService dataObjectMetaDataService) {
1082         this.dataObjectMetaDataService = dataObjectMetaDataService;
1083     }
1084 
1085     public DocumentDictionaryService getDocumentDictionaryService() {
1086         if (documentDictionaryService == null) {
1087             documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1088         }
1089         return documentDictionaryService;
1090     }
1091 
1092     public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1093         this.documentDictionaryService = documentDictionaryService;
1094     }
1095 
1096     protected LookupService getLookupService() {
1097         if (lookupService == null) {
1098             this.lookupService = KRADServiceLocatorWeb.getLookupService();
1099         }
1100         return lookupService;
1101     }
1102 
1103     public void setLookupService(LookupService lookupService) {
1104         this.lookupService = lookupService;
1105     }
1106 
1107     protected EncryptionService getEncryptionService() {
1108         if (encryptionService == null) {
1109             this.encryptionService = CoreApiServiceLocator.getEncryptionService();
1110         }
1111         return encryptionService;
1112     }
1113 
1114     public void setEncryptionService(EncryptionService encryptionService) {
1115         this.encryptionService = encryptionService;
1116     }
1117 }