001 /**
002 * Copyright 2005-2012 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kew.impl.document.search;
017
018 import org.apache.commons.beanutils.PropertyUtils;
019 import org.apache.commons.lang.ArrayUtils;
020 import org.apache.commons.lang.BooleanUtils;
021 import org.apache.commons.lang.ObjectUtils;
022 import org.apache.commons.lang.StringUtils;
023 import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
024 import org.kuali.rice.core.api.CoreApiServiceLocator;
025 import org.kuali.rice.core.api.config.property.Config;
026 import org.kuali.rice.core.api.config.property.ConfigContext;
027 import org.kuali.rice.core.api.search.SearchOperator;
028 import org.kuali.rice.core.api.uif.RemotableAttributeField;
029 import org.kuali.rice.core.api.util.KeyValue;
030 import org.kuali.rice.core.api.util.RiceKeyConstants;
031 import org.kuali.rice.core.web.format.Formatter;
032 import org.kuali.rice.kew.api.KEWPropertyConstants;
033 import org.kuali.rice.kew.api.KewApiConstants;
034 import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
035 import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
036 import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
037 import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
038 import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
039 import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor;
040 import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter;
041 import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
042 import org.kuali.rice.kew.doctype.bo.DocumentType;
043 import org.kuali.rice.kew.exception.WorkflowServiceError;
044 import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
045 import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
046 import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration;
047 import org.kuali.rice.kew.framework.document.search.StandardResultField;
048 import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder;
049 import org.kuali.rice.kew.service.KEWServiceLocator;
050 import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
051 import org.kuali.rice.kns.lookup.HtmlData;
052 import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl;
053 import org.kuali.rice.kns.lookup.LookupUtils;
054 import org.kuali.rice.kns.util.FieldUtils;
055 import org.kuali.rice.kns.web.struts.form.LookupForm;
056 import org.kuali.rice.kns.web.ui.Column;
057 import org.kuali.rice.kns.web.ui.Field;
058 import org.kuali.rice.kns.web.ui.ResultRow;
059 import org.kuali.rice.kns.web.ui.Row;
060 import org.kuali.rice.krad.bo.BusinessObject;
061 import org.kuali.rice.krad.exception.ValidationException;
062 import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
063 import org.kuali.rice.krad.util.GlobalVariables;
064 import org.kuali.rice.krad.util.KRADConstants;
065
066 import java.lang.reflect.InvocationTargetException;
067 import java.text.MessageFormat;
068 import java.util.ArrayList;
069 import java.util.Collection;
070 import java.util.Collections;
071 import java.util.HashMap;
072 import java.util.List;
073 import java.util.Map;
074
075 /**
076 * Implementation of lookupable helper service which handles the complex lookup behavior required by the KEW
077 * document search screen.
078 *
079 * @author Kuali Rice Team (rice.collab@kuali.org)
080 */
081 public class DocumentSearchCriteriaBoLookupableHelperService extends KualiLookupableHelperServiceImpl {
082
083 private static final String DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX = "documentAttribute.";
084
085 static final String SAVED_SEARCH_NAME_PARAM = "savedSearchToLoadAndExecute";
086
087 // warning message keys
088
089 private static final String EXCEED_THRESHOLD_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThreshold";
090 private static final String SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.securityFiltered";
091 private static final String EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThresholdAndSecurityFiltered";
092
093 private static final boolean DOCUMENT_HANDLER_POPUP_DEFAULT = true;
094 private static final boolean ROUTE_LOG_POPUP_DEFAULT = true;
095
096 // injected services
097
098 private DocumentSearchService documentSearchService;
099 private DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor;
100 private DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator;
101
102 // unfortunately, lookup helpers are stateful, need to store these here for other methods to use
103 protected DocumentSearchResults searchResults = null;
104 protected DocumentSearchCriteria criteria = null;
105
106 @Override
107 protected List<? extends BusinessObject> getSearchResultsHelper(Map<String, String> fieldValues,
108 boolean unbounded) {
109 criteria = loadCriteria(fieldValues);
110 searchResults = null;
111 try {
112 searchResults = KEWServiceLocator.getDocumentSearchService().lookupDocuments(
113 GlobalVariables.getUserSession().getPrincipalId(), criteria);
114 if (searchResults.isCriteriaModified()) {
115 criteria = searchResults.getCriteria();
116 }
117 } catch (WorkflowServiceErrorException wsee) {
118 for (WorkflowServiceError workflowServiceError : (List<WorkflowServiceError>) wsee.getServiceErrors()) {
119 if (workflowServiceError.getMessageMap() != null && workflowServiceError.getMessageMap().hasErrors()) {
120 // merge the message maps
121 GlobalVariables.getMessageMap().merge(workflowServiceError.getMessageMap());
122 } else {
123 GlobalVariables.getMessageMap().putError(workflowServiceError.getMessage(),
124 RiceKeyConstants.ERROR_CUSTOM, workflowServiceError.getMessage());
125 }
126 }
127 }
128
129 if (!GlobalVariables.getMessageMap().hasNoErrors() || searchResults == null) {
130 throw new ValidationException("error with doc search");
131 }
132
133 populateResultWarningMessages(searchResults);
134
135 List<DocumentSearchResult> individualSearchResults = searchResults.getSearchResults();
136
137 setBackLocation(fieldValues.get(KRADConstants.BACK_LOCATION));
138 setDocFormKey(fieldValues.get(KRADConstants.DOC_FORM_KEY));
139
140 applyCriteriaChangesToFields(criteria);
141
142 return populateSearchResults(individualSearchResults);
143
144 }
145
146 /**
147 * Inspects the lookup results to determine if any warning messages should be published to the message map.
148 */
149 protected void populateResultWarningMessages(DocumentSearchResults searchResults) {
150 // check various warning conditions
151 boolean overThreshold = searchResults.isOverThreshold();
152 int numFiltered = searchResults.getNumberOfSecurityFilteredResults();
153 int numResults = searchResults.getSearchResults().size();
154 if (overThreshold && numFiltered > 0) {
155 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
156 EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY,
157 String.valueOf(numResults),
158 String.valueOf(numFiltered));
159 } else if (numFiltered > 0) {
160 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
161 SECURITY_FILTERED_MESSAGE_KEY,
162 String.valueOf(numFiltered));
163 } else if (overThreshold) {
164 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
165 EXCEED_THRESHOLD_MESSAGE_KEY,
166 String.valueOf(numResults));
167 }
168 }
169
170 /**
171 * Applies changes that might have happened to the criteria back to the fields so that they show up on the form.
172 * Namely, this handles populating the form with today's date if the create date was not filled in on the form.
173 */
174 protected void applyCriteriaChangesToFields(DocumentSearchCriteriaContract criteria) {
175 for (Field field: getFormFields()) {
176 if(StringUtils.equals(field.getPropertyName(), KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + "dateCreated") && StringUtils.isEmpty(field.getPropertyValue())) {
177 if (criteria.getDateCreatedFrom() != null) {
178 field.setPropertyValue(CoreApiServiceLocator.getDateTimeService().toDateString(criteria.getDateCreatedFrom().toDate()));
179 }
180 }
181 }
182 }
183
184 /**
185 * Cleans up various issues with fieldValues coming from the lookup form (namely, that they don't include
186 * multi-valued field values!). Handles these by adding them comma-separated.
187 */
188 protected Map<String, String> cleanupFieldValues(Map<String, String> fieldValues, Map<String, String[]> parameters) {
189 Map<String, String> cleanedUpFieldValues = new HashMap<String, String>(fieldValues);
190 if (ArrayUtils.isNotEmpty(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE))) {
191 cleanedUpFieldValues.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE,
192 StringUtils.join(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE), ","));
193 }
194 Map<String, String> documentAttributeFieldValues = new HashMap<String, String>();
195 for (String parameterName : parameters.keySet()) {
196 if (parameterName.contains(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) {
197 String[] value = parameters.get(parameterName);
198 if (ArrayUtils.isNotEmpty(value)) {
199 documentAttributeFieldValues.put(parameterName, StringUtils.join(value, " " + SearchOperator.OR.op() + " "));
200 }
201 }
202 }
203 // if any of the document attributes are range values, process them
204 documentAttributeFieldValues.putAll(LookupUtils.preProcessRangeFields(documentAttributeFieldValues));
205 cleanedUpFieldValues.putAll(documentAttributeFieldValues);
206 return cleanedUpFieldValues;
207 }
208
209 /**
210 * Loads the document search criteria from the given map of field values as submitted from the search screen, and
211 * populates the current form Rows/Fields with the saved criteria fields
212 */
213 protected DocumentSearchCriteria loadCriteria(Map<String, String> fieldValues) {
214 fieldValues = cleanupFieldValues(fieldValues, getParameters());
215 String[] savedSearchToLoad = getParameters().get(SAVED_SEARCH_NAME_PARAM);
216 boolean savedSearch = savedSearchToLoad != null && savedSearchToLoad.length > 0 && StringUtils.isNotBlank(savedSearchToLoad[0]);
217 if (savedSearch) {
218 DocumentSearchCriteria criteria = getDocumentSearchService().getSavedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchToLoad[0]);
219 if (criteria != null) {
220 mergeFieldValues(getDocumentSearchCriteriaTranslator().translateCriteriaToFields(criteria));
221 return criteria;
222 }
223 }
224 // either it wasn't a saved search or the saved search failed to resolve
225 return getDocumentSearchCriteriaTranslator().translateFieldsToCriteria(fieldValues);
226 }
227
228 protected List<DocumentSearchCriteriaBo> populateSearchResults(List<DocumentSearchResult> lookupResults) {
229 List<DocumentSearchCriteriaBo> searchResults = new ArrayList<DocumentSearchCriteriaBo>();
230 for (DocumentSearchResult searchResult : lookupResults) {
231 DocumentSearchCriteriaBo result = new DocumentSearchCriteriaBo();
232 result.populateFromDocumentSearchResult(searchResult);
233 searchResults.add(result);
234 }
235 return searchResults;
236 }
237
238 @Override
239 public Collection performLookup(LookupForm lookupForm, Collection resultTable, boolean bounded) {
240 Collection lookupResult = super.performLookup(lookupForm, resultTable, bounded);
241 postProcessResults(resultTable, this.searchResults);
242 return lookupResult;
243 }
244
245 /**
246 * Sets a Field value appropriately, depending on whether it is a "multi-value" field type
247 */
248 protected static void setFieldValue(Field field, String[] values) {
249 if(!Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
250 field.setPropertyValue(values[0]);
251 } else {
252 //multi value, set to values
253 field.setPropertyValues(values);
254 }
255 }
256
257 /**
258 * Preserves Field values, saving single or array property value depending on field type; single property value is
259 * converted into String[1]
260 * This implementation makes the assumption that a Field can either represent a single property value, or an array
261 * of values but not both! (only one is preserved)
262 * @return a Map<String, String[]> containing field values depending on field type
263 */
264 protected Map<String, String[]> getFormFieldsValues() {
265 Map<String, String[]> values = new HashMap<String, String[]>();
266 for (Field field : getFormFields()) {
267 String[] value;
268 if(!Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
269 value = new String[] { field.getPropertyValue() };
270 } else {
271 //multi value, set to values
272 value = field.getPropertyValues();
273 }
274 values.put(field.getPropertyName(), value);
275 }
276 return values;
277 }
278
279 /**
280 * Overrides a Field value; sets a fallback/restored value if there is no new value
281 */
282 protected static void overrideFieldValue(Field field, Map<String, String[]> newValues, Map<String, String[]> oldValues) {
283 if (StringUtils.isNotBlank(field.getPropertyName())) {
284 if (newValues.get(field.getPropertyName()) != null) {
285 setFieldValue(field, newValues.get(field.getPropertyName()));
286 } else if (oldValues.get(field.getPropertyName()) != null) {
287 setFieldValue(field, oldValues.get(field.getPropertyName()));
288 }
289 }
290 }
291
292 /**
293 * Overrides Row Field values with Map values
294 * @param values
295 */
296 protected void mergeFieldValues(Map<String, String[]> values) {
297 for (Field field: getFormFields()) {
298 if (StringUtils.isNotBlank(field.getPropertyName())) {
299 if (values.get(field.getPropertyName()) != null) {
300 setFieldValue(field, values.get(field.getPropertyName()));
301 }
302 }
303 }
304 }
305
306 /**
307 * Sets a single form field
308 */
309 protected void setFormField(String name, String value) {
310 for (Field field : getFormFields()) {
311 if(StringUtils.equals(field.getPropertyName(), name)) {
312 setFieldValue(field, new String[] { value });
313 break;
314 }
315 }
316 }
317
318 /**
319 * Handles toggling between form views.
320 * Reads and sets the Rows state.
321 */
322 protected void toggleFormView() {
323 Map<String,String[]> fieldValues = new HashMap<String,String[]>();
324 Map<String, String[]> savedValues = getFormFieldsValues();
325
326 // the original implementation saved the form values and then re-applied them
327 // we do the same here, however I suspect we may be able to avoid this re-application
328 // of existing values
329
330 for (Field field: getFormFields()) {
331 overrideFieldValue(field, this.getParameters(), savedValues);
332 // if we are sure this does not depend on or cause side effects in other fields
333 // then this phase can be extracted and these loops simplified
334 applyFieldAuthorizationsFromNestedLookups(field);
335 fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() });
336 }
337
338 // checkForAdditionalFields generates the form (setRows)
339 if (checkForAdditionalFields(fieldValues)) {
340 for (Field field: getFormFields()) {
341 overrideFieldValue(field, this.getParameters(), savedValues);
342 fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() });
343 }
344 }
345
346 // unset the clear search param, since this is not really a state, but just an action
347 // it can never be toggled "off", just "on"
348 setFormField(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, "");
349 }
350
351 /**
352 * Loads a saved search
353 * @return returns true on success to run the loaded search, false on error.
354 */
355 protected boolean loadSavedSearch(boolean ignoreErrors) {
356 Map<String,String[]> fieldValues = new HashMap<String,String[]>();
357
358 String savedSearchName = getSavedSearchName();
359 if(StringUtils.isEmpty(savedSearchName) || "*ignore*".equals(savedSearchName)) {
360 if(!ignoreErrors) {
361 GlobalVariables.getMessageMap().putError(SAVED_SEARCH_NAME_PARAM, RiceKeyConstants.ERROR_CUSTOM, "You must select a saved search");
362 } else {
363 //if we're ignoring errors and we got an error just return, no reason to continue. Also set false to indicate not to perform lookup
364 return false;
365 }
366 setFormField(SAVED_SEARCH_NAME_PARAM, "");
367 }
368 if (!GlobalVariables.getMessageMap().hasNoErrors()) {
369 throw new ValidationException("errors in search criteria");
370 }
371
372 DocumentSearchCriteria criteria = KEWServiceLocator.getDocumentSearchService().getSavedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchName);
373
374 // get the document type
375 String docTypeName = criteria.getDocumentTypeName();
376
377 // update the parameters to include whether or not this is an advanced search
378 if(this.getParameters().containsKey(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD)) {
379 Map<String, String[]> parameters = this.getParameters();
380 String[] params = (String[])parameters.get(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD);
381 if (ArrayUtils.isNotEmpty(params)) {
382 params[0] = criteria.getIsAdvancedSearch();
383 this.setParameters(parameters);
384 }
385 }
386
387 // and set the rows based on doc type
388 setRows(docTypeName);
389
390 // clear the name of the search in the form
391 //fieldValues.put(SAVED_SEARCH_NAME_PARAM, new String[0]);
392
393 // set the custom document attribute values on the search form
394 for (Map.Entry<String, List<String>> entry: criteria.getDocumentAttributeValues().entrySet()) {
395 fieldValues.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
396 }
397
398 // sets the field values on the form, trying criteria object properties if a field value is not present in the map
399 for (Field field : getFormFields()) {
400 if (field.getPropertyName() != null && !field.getPropertyName().equals("")) {
401 // UI Fields know whether they are single or multiple value
402 // just set both so they can make the determination and render appropriately
403 String[] values = null;
404 if (fieldValues.get(field.getPropertyName()) != null) {
405 values = fieldValues.get(field.getPropertyName());
406 } else {
407 //may be on the root of the criteria object, try looking there:
408 try {
409 values = new String[] { ObjectUtils.toString(PropertyUtils.getProperty(criteria, field.getPropertyName())) };
410 } catch (IllegalAccessException e) {
411 e.printStackTrace();
412 } catch (InvocationTargetException e) {
413 e.printStackTrace();
414 } catch (NoSuchMethodException e) {
415 // e.printStackTrace();
416 //hmm what to do here, we should be able to find everything either in the search atts or at the base as far as I know.
417 }
418 }
419 if (values != null) {
420 setFieldValue(field, values);
421 }
422 }
423 }
424
425 return true;
426 }
427
428 /**
429 * Performs custom document search/lookup actions.
430 * 1) switching between simple/detailed search
431 * 2) switching between non-superuser/superuser search
432 * 3) clearing saved search results
433 * 4) restoring a saved search and executing the search
434 * @param ignoreErrors
435 * @return whether to rerun the previous search; false in cases 1-3 because we are just updating the form
436 */
437 @Override
438 public boolean performCustomAction(boolean ignoreErrors) {
439 //boolean isConfigAction = isAdvancedSearch() || isSuperUserSearch() || isClearSavedSearch();
440 if (isClearSavedSearch()) {
441 KEWServiceLocator.getDocumentSearchService().clearNamedSearches(GlobalVariables.getUserSession().getPrincipalId());
442 return false;
443 }
444 else if (getSavedSearchName() != null) {
445 return loadSavedSearch(ignoreErrors);
446 } else {
447 toggleFormView();
448 // Finally, return false to prevent the search from being performed and to skip the other custom processing below.
449 return false;
450 }
451 }
452
453 /**
454 * Custom implementation of getInquiryUrl that sets up doc handler link.
455 */
456 @Override
457 public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) {
458 DocumentSearchCriteriaBo criteriaBo = (DocumentSearchCriteriaBo)bo;
459 if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOCUMENT_ID.equals(propertyName)) {
460 return generateDocumentHandlerUrl(criteriaBo.getDocumentId(), criteriaBo.getDocumentType(),
461 isSuperUserSearch());
462 } else if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG.equals(propertyName)) {
463 return generateRouteLogUrl(criteriaBo.getDocumentId());
464 }
465 return super.getInquiryUrl(bo, propertyName);
466 }
467
468 /**
469 * Generates the appropriate document handler url for the given document. If superUserSearch is true then a super
470 * user doc handler link will be generated if the document type policy allows it.
471 */
472 protected HtmlData.AnchorHtmlData generateDocumentHandlerUrl(String documentId, DocumentType documentType, boolean superUserSearch) {
473 HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
474 link.setDisplayText(documentId);
475 if (isDocumentHandlerPopup()) {
476 link.setTarget("_blank");
477 }
478 String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/";
479 if (superUserSearch) {
480 if (documentType.getUseWorkflowSuperUserDocHandlerUrl().getPolicyValue().booleanValue()) {
481 url += "SuperUser.do?methodToCall=displaySuperUserDocument&documentId=" + documentId;
482 } else {
483 url = KewApiConstants.DOC_HANDLER_REDIRECT_PAGE
484 + "?" + KewApiConstants.COMMAND_PARAMETER + "="
485 + KewApiConstants.SUPERUSER_COMMAND + "&"
486 + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
487 + documentId;
488 }
489 } else {
490 url += KewApiConstants.DOC_HANDLER_REDIRECT_PAGE + "?"
491 + KewApiConstants.COMMAND_PARAMETER + "="
492 + KewApiConstants.DOCSEARCH_COMMAND + "&"
493 + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
494 + documentId;
495 }
496 link.setHref(url);
497 return link;
498 }
499
500 protected HtmlData.AnchorHtmlData generateRouteLogUrl(String documentId) {
501 HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
502 if (isRouteLogPopup()) {
503 link.setTarget("_blank");
504 }
505 link.setDisplayText("Route Log for document " + documentId);
506 String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/" +
507 "RouteLog.do?documentId=" + documentId;
508 link.setHref(url);
509 return link;
510 }
511
512 /**
513 * Returns true if the document handler should open in a new window.
514 */
515 protected boolean isDocumentHandlerPopup() {
516 return BooleanUtils.toBooleanDefaultIfNull(
517 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
518 KewApiConstants.KEW_NAMESPACE,
519 KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
520 KewApiConstants.DOCUMENT_SEARCH_DOCUMENT_POPUP_IND),
521 DOCUMENT_HANDLER_POPUP_DEFAULT);
522 }
523
524 /**
525 * Returns true if the route log should open in a new window.
526 */
527 public boolean isRouteLogPopup() {
528 return BooleanUtils.toBooleanDefaultIfNull(
529 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(KewApiConstants.KEW_NAMESPACE,
530 KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
531 KewApiConstants.DOCUMENT_SEARCH_ROUTE_LOG_POPUP_IND), ROUTE_LOG_POPUP_DEFAULT);
532 }
533
534 /**
535 * Parses a boolean request parameter
536 */
537 protected boolean isFlagSet(String flagName) {
538 if(this.getParameters().containsKey(flagName)) {
539 String[] params = (String[])this.getParameters().get(flagName);
540 if (ArrayUtils.isNotEmpty(params)) {
541 return "YES".equalsIgnoreCase(params[0]);
542 }
543 }
544 return false;
545 }
546
547 /**
548 * Returns true if the current search being executed is a super user search.
549 */
550 protected boolean isSuperUserSearch() {
551 return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD);
552 }
553
554 /**
555 * Returns true if the current search being executed is an "advanced" search.
556 */
557 protected boolean isAdvancedSearch() {
558 return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD);
559 }
560
561 /**
562 * Returns true if the current "search" being executed is an "clear" search.
563 */
564 protected boolean isClearSavedSearch() {
565 return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD);
566 }
567
568 protected String getSavedSearchName() {
569 String[] savedSearchName = getParameters().get(SAVED_SEARCH_NAME_PARAM);
570 if (savedSearchName != null && savedSearchName.length > 0) {
571 return savedSearchName[0];
572 }
573 return null;
574 }
575
576 /**
577 * Override setRows in order to post-process and add documenttype-dependent fields
578 */
579 @Override
580 protected void setRows() {
581 this.setRows(null);
582 }
583
584 /**
585 * Returns an iterable of current form fields
586 */
587 protected Iterable<Field> getFormFields() {
588 return getFields(this.getRows());
589 }
590
591 /**
592 * Sets the rows for the search criteria. This method will delegate to the DocumentSearchCriteriaProcessor
593 * in order to pull in fields for custom search attributes.
594 *
595 * @param documentTypeName the name of the document type currently entered on the form, if this is a valid document
596 * type then it may have search attribute fields that need to be displayed; documentType name may also be loaded
597 * via a saved search
598 */
599 protected void setRows(String documentTypeName) {
600 // Always call superclass to regenerate the rows since state may have changed (namely, documentTypeName parsed from params)
601 super.setRows();
602
603 List<Row> lookupRows = new ArrayList<Row>();
604 //copy the current rows
605 for (Row row : getRows()) {
606 lookupRows.add(row);
607 }
608 //clear out
609 getRows().clear();
610
611 DocumentType docType = getValidDocumentType(documentTypeName);
612
613 boolean advancedSearch = isAdvancedSearch();
614 boolean superUserSearch = isSuperUserSearch();
615
616 //call get rows
617 List<Row> rows = getDocumentSearchCriteriaProcessor().getRows(docType,lookupRows, advancedSearch, superUserSearch);
618
619 BusinessObjectEntry boe = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(this.getBusinessObjectClass().getName());
620 int numCols = boe.getLookupDefinition().getNumOfColumns();
621 if(numCols == 0) {
622 numCols = KRADConstants.DEFAULT_NUM_OF_COLUMNS;
623 }
624
625 super.getRows().addAll(FieldUtils.wrapFields(this.getFields(rows), numCols));
626
627 }
628
629 private static List<Field> getFields(Collection<? extends Row> rows) {
630 List<Field> rList = new ArrayList<Field>();
631 for (Row r : rows) {
632 for (Field f : r.getFields()) {
633 rList.add(f);
634 }
635 }
636 return rList;
637 }
638
639 /**
640 * Checks for a valid document type with the given name in a case-sensitive manner.
641 *
642 * @return the DocumentType which matches the given name or null if no valid document type could be found
643 */
644 private DocumentType getValidDocumentType(String documentTypeName) {
645 if (StringUtils.isNotEmpty(documentTypeName)) {
646 DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName.trim());
647 if (documentType != null && documentType.isActive()) {
648 return documentType;
649 }
650 }
651 return null;
652 }
653
654 private static String TOGGLE_BUTTON = "<input type='image' name=''{0}'' id=''{0}'' class='tinybutton' src=''..{1}/images/tinybutton-{2}search.gif'' alt=''{3} search'' title=''{3} search''/>";
655
656 @Override
657 public String getSupplementalMenuBar() {
658 boolean advancedSearch = isAdvancedSearch();
659 boolean superUserSearch = isSuperUserSearch();
660 StringBuilder suppMenuBar = new StringBuilder();
661
662 // Add the detailed-search-toggling button.
663 // to mimic previous behavior, basic search button is shown both when currently rendering detailed search AND super user search
664 // as super user search is essentially a detailed search
665 String type = advancedSearch ? "basic" : "detailed";
666 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleAdvancedSearch", KewApiConstants.WEBAPP_DIRECTORY, type, type));
667
668 // Add the superuser-search-toggling button.
669 suppMenuBar.append(" ");
670 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleSuperUserSearch", KewApiConstants.WEBAPP_DIRECTORY, superUserSearch ? "nonsupu" : "superuser", superUserSearch ? "non-superuser" : "superuser"));
671
672 // Add the "clear saved searches" button.
673 suppMenuBar.append(" ");
674 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, KewApiConstants.WEBAPP_DIRECTORY, "clearsaved", "clear saved searches"));
675
676 // Wire up the onblur for document type name
677 suppMenuBar.append("<script type=\"text/javascript\">"
678 + " jQuery(document).ready(function () {"
679 + " jQuery(\"#documentTypeName\").blur(function () { validateDocTypeAndRefresh( this ); });"
680 + "});</script>");
681
682 return suppMenuBar.toString();
683 }
684
685 @Override
686 public boolean shouldDisplayHeaderNonMaintActions() {
687 return true;
688 }
689
690 @Override
691 public boolean shouldDisplayLookupCriteria() {
692 return true;
693 }
694
695 /**
696 * Determines if there should be more search fields rendered based on already entered search criteria, and
697 * generates additional form rows.
698 */
699 @Override
700 public boolean checkForAdditionalFields(Map fieldValues) {
701 // The given map is a Map<String, String>
702 Object val = fieldValues.get("documentTypeName");
703 String documentTypeName;
704 if (val instanceof String[]) {
705 documentTypeName = ((String[]) val)[0];
706 } else {
707 documentTypeName = (String) val;
708 }
709 if (StringUtils.isNotBlank(documentTypeName)) {
710 setRows(documentTypeName);
711 }
712 return true;
713 }
714
715 @Override
716 public Field getExtraField() {
717 SavedSearchValuesFinder savedSearchValuesFinder = new SavedSearchValuesFinder();
718 List<KeyValue> savedSearchValues = savedSearchValuesFinder.getKeyValues();
719 Field savedSearch = new Field();
720 savedSearch.setPropertyName(SAVED_SEARCH_NAME_PARAM);
721 savedSearch.setFieldType(Field.DROPDOWN_SCRIPT);
722 savedSearch.setScript("customLookupChanged()");
723 savedSearch.setFieldValidValues(savedSearchValues);
724 savedSearch.setFieldLabel("Saved Searches");
725 return savedSearch;
726 }
727
728 @Override
729 public void performClear(LookupForm lookupForm) {
730 DocumentSearchCriteria criteria = loadCriteria(lookupForm.getFields());
731 super.performClear(lookupForm);
732 repopulateSearchTypeFlags();
733 DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
734 if (documentType != null) {
735 DocumentSearchCriteria clearedCriteria = documentSearchService.clearCriteria(documentType, criteria);
736 applyCriteriaChangesToFields(DocumentSearchCriteria.Builder.create(clearedCriteria));
737 }
738 }
739
740 /**
741 * Repopulate the fields indicating advanced/superuser search type.
742 */
743 protected void repopulateSearchTypeFlags() {
744 boolean advancedSearch = isAdvancedSearch();
745 boolean superUserSearch = isSuperUserSearch();
746 int fieldsRepopulated = 0;
747 for (Field field: getFields(super.getRows())) {
748 if (fieldsRepopulated >= 2) {
749 break;
750 }
751 if (DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD.equals(field.getPropertyName())) {
752 field.setPropertyValue(advancedSearch ? "YES" : "NO");
753 fieldsRepopulated++;
754 } else if (DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD.equals(field.getPropertyName())) {
755 field.setPropertyValue(advancedSearch ? "YES" : "NO");
756 fieldsRepopulated++;
757 }
758 }
759
760 }
761
762 /**
763 * Takes a collection of result rows and does final processing on them.
764 */
765 protected void postProcessResults(Collection<ResultRow> resultRows, DocumentSearchResults searchResults) {
766 if (resultRows.size() != searchResults.getSearchResults().size()) {
767 throw new IllegalStateException("Encountered a mismatch between ResultRow items and document search results "
768 + resultRows.size() + " != " + searchResults.getSearchResults().size());
769 }
770 DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
771 DocumentSearchResultSetConfiguration resultSetConfiguration = null;
772 DocumentSearchCriteriaConfiguration criteriaConfiguration = null;
773 if (documentType != null) {
774 resultSetConfiguration =
775 KEWServiceLocator.getDocumentSearchCustomizationMediator().customizeResultSetConfiguration(
776 documentType, criteria);
777 criteriaConfiguration =
778 KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(
779 documentType);
780
781 }
782 int index = 0;
783 for (ResultRow resultRow : resultRows) {
784 DocumentSearchResult searchResult = searchResults.getSearchResults().get(index);
785 executeColumnCustomization(resultRow, searchResult, resultSetConfiguration, criteriaConfiguration);
786 index++;
787 }
788 }
789
790 /**
791 * Executes customization of columns, could include removing certain columns or adding additional columns to the
792 * result row (in cases where columns are added by document search customization, such as searchable attributes).
793 */
794 protected void executeColumnCustomization(ResultRow resultRow, DocumentSearchResult searchResult,
795 DocumentSearchResultSetConfiguration resultSetConfiguration,
796 DocumentSearchCriteriaConfiguration criteriaConfiguration) {
797 if (resultSetConfiguration == null) {
798 resultSetConfiguration = DocumentSearchResultSetConfiguration.Builder.create().build();
799 }
800 if (criteriaConfiguration == null) {
801 criteriaConfiguration = DocumentSearchCriteriaConfiguration.Builder.create().build();
802 }
803 List<StandardResultField> standardFieldsToRemove = resultSetConfiguration.getStandardResultFieldsToRemove();
804 if (standardFieldsToRemove == null) {
805 standardFieldsToRemove = Collections.emptyList();
806 }
807 List<Column> newColumns = new ArrayList<Column>();
808 for (Column standardColumn : resultRow.getColumns()) {
809 if (!standardFieldsToRemove.contains(StandardResultField.fromFieldName(standardColumn.getPropertyName()))) {
810 newColumns.add(standardColumn);
811 // modify the route log column so that xml values are not escaped (allows for the route log <img ...> to be
812 // rendered properly)
813 if (standardColumn.getPropertyName().equals(
814 KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG)) {
815 standardColumn.setEscapeXMLValue(false);
816 }
817 }
818 }
819
820 // determine which document attribute fields should be added
821 List<RemotableAttributeField> searchAttributeFields = criteriaConfiguration.getFlattenedSearchAttributeFields();
822 List<String> additionalFieldNamesToInclude = new ArrayList<String>();
823 if (!resultSetConfiguration.isOverrideSearchableAttributes()) {
824 for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
825 // TODO - KULRICE-5738 - add check here to make sure the searchable attribute should be displayed in result set
826 // right now this is default always including all searchable attributes!
827 additionalFieldNamesToInclude.add(searchAttributeField.getName());
828 }
829 }
830 if (resultSetConfiguration.getCustomFieldNamesToAdd() != null) {
831 additionalFieldNamesToInclude.addAll(resultSetConfiguration.getCustomFieldNamesToAdd());
832 }
833
834 // now assemble the custom columns
835 List<Column> customColumns = new ArrayList<Column>();
836 List<Column> additionalAttributeColumns = FieldUtils.constructColumnsFromAttributeFields(
837 resultSetConfiguration.getAdditionalAttributeFields());
838
839 outer:for (String additionalFieldNameToInclude : additionalFieldNamesToInclude) {
840 // search the search attribute fields
841 for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
842 if (additionalFieldNameToInclude.equals(searchAttributeField.getName())) {
843 Column searchAttributeColumn = FieldUtils.constructColumnFromAttributeField(searchAttributeField);
844 wrapDocumentAttributeColumnName(searchAttributeColumn);
845 customColumns.add(searchAttributeColumn);
846 continue outer;
847 }
848 }
849 for (Column additionalAttributeColumn : additionalAttributeColumns) {
850 if (additionalFieldNameToInclude.equals(additionalAttributeColumn.getPropertyName())) {
851 wrapDocumentAttributeColumnName(additionalAttributeColumn);
852 customColumns.add(additionalAttributeColumn);
853 continue outer;
854 }
855 }
856 LOG.warn("Failed to locate a proper column definition for requested additional field to include in"
857 + "result set with name '"
858 + additionalFieldNameToInclude
859 + "'");
860 }
861 populateCustomColumns(customColumns, searchResult);
862
863 // now merge the custom columns into the standard columns right before the route log (if the route log column wasn't removed!)
864 if (newColumns.isEmpty() || !StandardResultField.ROUTE_LOG.isFieldNameValid(newColumns.get(newColumns.size() - 1).getPropertyName())) {
865 newColumns.addAll(customColumns);
866 } else {
867 newColumns.addAll(newColumns.size() - 1, customColumns);
868 }
869 resultRow.setColumns(newColumns);
870 }
871
872 protected void populateCustomColumns(List<Column> customColumns, DocumentSearchResult searchResult) {
873 for (Column customColumn : customColumns) {
874 DocumentAttribute documentAttribute =
875 searchResult.getSingleDocumentAttributeByName(customColumn.getPropertyName());
876 if (documentAttribute != null && documentAttribute.getValue() != null) {
877 wrapDocumentAttributeColumnName(customColumn);
878 // list moving forward if the attribute has more than one value
879 Formatter formatter = customColumn.getFormatter();
880 customColumn.setPropertyValue(formatter.format(documentAttribute.getValue()).toString());
881 }
882 }
883 }
884
885 private void wrapDocumentAttributeColumnName(Column column) {
886 // TODO - comment out for now, not sure we really want to do this...
887 //column.setPropertyName(DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX + column.getPropertyName());
888 }
889
890 public void setDocumentSearchService(DocumentSearchService documentSearchService) {
891 this.documentSearchService = documentSearchService;
892 }
893
894 public DocumentSearchService getDocumentSearchService() {
895 return documentSearchService;
896 }
897
898 public DocumentSearchCriteriaProcessor getDocumentSearchCriteriaProcessor() {
899 return documentSearchCriteriaProcessor;
900 }
901
902 public void setDocumentSearchCriteriaProcessor(DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor) {
903 this.documentSearchCriteriaProcessor = documentSearchCriteriaProcessor;
904 }
905
906 public DocumentSearchCriteriaTranslator getDocumentSearchCriteriaTranslator() {
907 return documentSearchCriteriaTranslator;
908 }
909
910 public void setDocumentSearchCriteriaTranslator(DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator) {
911 this.documentSearchCriteriaTranslator = documentSearchCriteriaTranslator;
912 }
913
914 }