View Javadoc

1   /**
2    * Copyright 2005-2012 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.kew.impl.document.search;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.ArrayUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.joda.time.DateTime;
22  import org.kuali.rice.core.api.CoreApiServiceLocator;
23  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
24  import org.kuali.rice.core.api.uif.RemotableAttributeError;
25  import org.kuali.rice.core.api.uif.RemotableAttributeField;
26  import org.kuali.rice.core.api.util.RiceConstants;
27  import org.kuali.rice.core.api.util.RiceKeyConstants;
28  import org.kuali.rice.core.framework.persistence.jdbc.sql.Criteria;
29  import org.kuali.rice.core.framework.persistence.jdbc.sql.SqlBuilder;
30  import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
31  import org.kuali.rice.kew.api.KewApiConstants;
32  import org.kuali.rice.kew.api.KewApiServiceLocator;
33  import org.kuali.rice.kew.api.document.Document;
34  import org.kuali.rice.kew.api.document.DocumentStatus;
35  import org.kuali.rice.kew.api.document.DocumentStatusCategory;
36  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
37  import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
38  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
39  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
40  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
41  import org.kuali.rice.kew.api.document.search.RouteNodeLookupLogic;
42  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
43  import org.kuali.rice.kew.docsearch.QueryComponent;
44  import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
45  import org.kuali.rice.kew.doctype.bo.DocumentType;
46  import org.kuali.rice.kew.doctype.service.DocumentTypeService;
47  import org.kuali.rice.kew.engine.node.RouteNode;
48  import org.kuali.rice.kew.service.KEWServiceLocator;
49  import org.kuali.rice.kew.util.PerformanceLogger;
50  import org.kuali.rice.kim.api.identity.Person;
51  import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
52  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
53  import org.kuali.rice.krad.util.GlobalVariables;
54  import org.kuali.rice.krad.util.MessageMap;
55  
56  import java.sql.ResultSet;
57  import java.sql.SQLException;
58  import java.sql.Statement;
59  import java.sql.Timestamp;
60  import java.util.ArrayList;
61  import java.util.Arrays;
62  import java.util.Collection;
63  import java.util.Collections;
64  import java.util.HashMap;
65  import java.util.HashSet;
66  import java.util.List;
67  import java.util.Map;
68  import java.util.Set;
69  import java.util.TreeSet;
70  
71  
72  /**
73   * Reference implementation of the {@code DocumentSearchGenerator}.
74   *
75   * @author Kuali Rice Team (rice.collab@kuali.org)
76   */
77  public class DocumentSearchGeneratorImpl implements DocumentSearchGenerator {
78  
79      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSearchGeneratorImpl.class);
80  
81      private static final String ROUTE_NODE_TABLE = "KREW_RTE_NODE_T";
82      private static final String ROUTE_NODE_INST_TABLE = "KREW_RTE_NODE_INSTN_T";
83      private static final String DATABASE_WILDCARD_CHARACTER_STRING = "%";
84      private static final char DATABASE_WILDCARD_CHARACTER = DATABASE_WILDCARD_CHARACTER_STRING.toCharArray()[0];
85  
86      private DatabasePlatform dbPlatform;
87      private MessageMap messageMap;
88  
89      private SqlBuilder sqlBuilder = null;
90  
91      @Override
92      public DocumentSearchCriteria clearSearch(DocumentSearchCriteria criteria) {
93          return DocumentSearchCriteria.Builder.create().build();
94      }
95  
96      public DocumentType getValidDocumentType(String documentTypeFullName) {
97          if (!org.apache.commons.lang.StringUtils.isEmpty(documentTypeFullName)) {
98              DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeFullName);
99              if (documentType == null) {
100                 throw new RuntimeException("No Valid Document Type Found for document type name '" + documentTypeFullName + "'");
101             }
102             return documentType;
103         }
104         return null;
105     }
106 
107     @Override
108     public List<RemotableAttributeError> validateSearchableAttributes(DocumentSearchCriteria.Builder criteria) {
109         List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
110         DocumentType documentType = null;
111         try{
112               documentType = getValidDocumentType(criteria.getDocumentTypeName());
113         }catch(RuntimeException re){
114             errors.add(RemotableAttributeError.Builder.create("documentTypeName", re.getMessage()).build());
115         }
116 
117         if (documentType != null) {
118             errors = KEWServiceLocator.getDocumentSearchCustomizationMediator().validateLookupFieldParameters(documentType, criteria.build());
119         }
120         return errors == null ? Collections.<RemotableAttributeError>emptyList() : Collections.unmodifiableList(errors);
121     }
122 
123     public QueryComponent getSearchableAttributeSql(Map<String, List<String>> documentAttributeValues, List<RemotableAttributeField> searchFields, String whereClausePredicatePrefix) {
124 
125         StringBuilder fromSql = new StringBuilder();
126         StringBuilder whereSql = new StringBuilder();
127 
128         //Map<String, List<SearchAttributeCriteriaComponent>> searchableAttributeRangeComponents = new HashMap<String,List<SearchAttributeCriteriaComponent>>();
129         Criteria finalCriteria = null;
130         int tableIndex = 1;
131         SqlBuilder sqlBuilder = this.getSqlBuilder();
132 
133         for (String documentAttributeName : documentAttributeValues.keySet()) {
134             String documentAttributeNameForSQL = documentAttributeName;
135             if (documentAttributeName.contains(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) {
136                 documentAttributeNameForSQL = documentAttributeName.replaceFirst(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX, "");
137             }
138             List<String> searchValues = documentAttributeValues.get(documentAttributeName);
139             if (CollectionUtils.isEmpty(searchValues)) {
140                 continue;
141             }
142 
143             String tableAlias = "EXT" + tableIndex;
144             RemotableAttributeField searchField = getSearchFieldByName(documentAttributeName, searchFields);
145             String tableName = DocumentSearchInternalUtils.getAttributeTableName(searchField);
146             boolean caseSensitive = DocumentSearchInternalUtils.isLookupCaseSensitive(searchField);
147 
148             Criteria crit = null;
149 
150             Class<?> dataTypeClass = DocumentSearchInternalUtils.getDataTypeClass(searchField);
151             if (searchValues.size() > 1) {
152                 // if there's more than one entry, we need to do an "in"
153                 crit = new Criteria(tableName, tableAlias);
154                 crit.setDbPlatform(sqlBuilder.getDbPlatform());
155                 crit.in("VAL", searchValues, dataTypeClass);
156             } else {
157                 crit = sqlBuilder.createCriteria("VAL", searchValues.get(0) , tableName, tableAlias, dataTypeClass, !caseSensitive);
158             }
159 
160             sqlBuilder.addCriteria("KEY_CD", documentAttributeNameForSQL, String.class, false, false, crit); // this is always of type string.
161             sqlBuilder.andCriteria("DOC_HDR_ID", tableAlias + ".DOC_HDR_ID", "KREW_DOC_HDR_T", "DOC_HDR", SqlBuilder.JoinType.class, false, false, crit);
162 
163             if (finalCriteria == null ){
164                 finalCriteria = crit;
165             } else{
166                 sqlBuilder.andCriteria(finalCriteria, crit);
167             }
168 
169             // - below is the old code
170             // if where clause is empty then use passed in prefix... otherwise generate one
171             String whereClausePrefix = (whereSql.length() == 0) ? whereClausePredicatePrefix : getGeneratedPredicatePrefix(whereSql.length());
172             QueryComponent qc = generateSearchableAttributeSql(tableName, documentAttributeNameForSQL, whereClausePrefix, tableIndex);
173             fromSql.append(qc.getFromSql());
174             tableIndex++;
175         }
176 
177         if (finalCriteria == null) {
178             return new QueryComponent("", "", "");
179         }
180 
181         String whereClausePrefix = (whereSql.length() == 0) ? whereClausePredicatePrefix : getGeneratedPredicatePrefix(whereSql.length());
182 
183         return new QueryComponent("", fromSql.toString(), whereClausePrefix + " " + finalCriteria.buildWhere());
184     }
185 
186     private RemotableAttributeField getSearchFieldByName(String fieldName, List<RemotableAttributeField> searchFields) {
187         for (RemotableAttributeField searchField : searchFields) {
188             if (searchField.getName().equals(fieldName)
189                     || searchField.getName().equals(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + fieldName)) {
190                 return searchField;
191             }
192         }
193         throw new IllegalStateException("Failed to locate a RemotableAttributeField for fieldName=" + fieldName);
194     }
195 
196     public QueryComponent generateSearchableAttributeSql(String tableName, String documentAttributeName, String whereSqlStarter,int tableIndex) {
197         String tableIdentifier = "EXT" + tableIndex;
198         QueryComponent joinSqlComponent = getSearchableAttributeJoinSql(tableName, tableIdentifier, whereSqlStarter, documentAttributeName);
199         return new QueryComponent("", joinSqlComponent.getFromSql(), joinSqlComponent.getWhereSql());
200     }
201 
202     public QueryComponent getSearchableAttributeJoinSql(String tableName, String tableIdentifier, String whereSqlStarter, String attributeTableKeyColumnName) {
203         return new QueryComponent("", generateSearchableAttributeFromSql(tableName, tableIdentifier).toString(), generateSearchableAttributeWhereClauseJoin(whereSqlStarter, tableIdentifier, attributeTableKeyColumnName).toString());
204     }
205 
206     public StringBuilder generateSearchableAttributeWhereClauseJoin(String whereSqlStarter,String tableIdentifier,String attributeTableKeyColumnName) {
207         StringBuilder whereSql = new StringBuilder(constructWhereClauseElement(whereSqlStarter, "DOC_HDR.DOC_HDR_ID", "=", getDbPlatform().escapeString(tableIdentifier + ".DOC_HDR_ID"), null, null));
208         whereSql.append(constructWhereClauseElement(" and ", tableIdentifier + ".KEY_CD", "=",
209                 getDbPlatform().escapeString(attributeTableKeyColumnName), "'", "'"));
210         return whereSql;
211     }
212 
213     public StringBuilder generateSearchableAttributeFromSql(String tableName, String tableIdentifier) {
214         if (StringUtils.isBlank(tableName)) {
215             throw new IllegalArgumentException("tableName was null or blank");
216         }
217         if (StringUtils.isBlank(tableIdentifier)) {
218             throw new IllegalArgumentException("tableIdentifier was null or blank");
219         }
220         StringBuilder fromSql = new StringBuilder();
221         fromSql.append(" ,").append(tableName).append(" ").append(getDbPlatform().escapeString(tableIdentifier)).append(" ");
222         return fromSql;
223     }
224 
225     public StringBuilder constructWhereClauseElement(String clauseStarter,String queryTableColumnName,String operand,String valueToSearch,String valuePrefix,String valueSuffix) {
226         StringBuilder whereSql = new StringBuilder();
227         valuePrefix = (valuePrefix != null) ? valuePrefix : "";
228         valueSuffix = (valueSuffix != null) ? valueSuffix : "";
229         whereSql.append(" " + clauseStarter + " ").append(getDbPlatform().escapeString(queryTableColumnName)).append(" " + operand + " ").append(valuePrefix).append(valueToSearch).append(valueSuffix).append(" ");
230         return whereSql;
231     }
232 
233     @Override
234     public DocumentSearchResults.Builder processResultSet(DocumentSearchCriteria criteria, boolean criteriaModified, Statement searchAttributeStatement, ResultSet resultSet, int maxResultCap, int fetchLimit) throws SQLException {
235         DocumentSearchCriteria.Builder criteriaBuilder = DocumentSearchCriteria.Builder.create(criteria);
236         DocumentSearchResults.Builder results = DocumentSearchResults.Builder.create(criteriaBuilder);
237         results.setCriteriaModified(criteriaModified);
238         int size = 0;
239         List<DocumentSearchResult.Builder> resultList = new ArrayList<DocumentSearchResult.Builder>();
240         results.setSearchResults(resultList);
241         Map<String, DocumentSearchResult.Builder> resultMap = new HashMap<String, DocumentSearchResult.Builder>();
242         PerformanceLogger perfLog = new PerformanceLogger();
243         int iteration = 0;
244         int startAt = (criteria.getStartAtIndex()==null) ? 0 : criteria.getStartAtIndex();
245         maxResultCap += startAt;
246         boolean resultSetHasNext = resultSet.next();
247         while ( resultSetHasNext && resultMap.size() < maxResultCap && iteration++ < fetchLimit && startAt >= 0) {
248 
249             if(iteration <= startAt) {
250                 resultSetHasNext = resultSet.next();
251                 continue;
252             }
253 
254             DocumentSearchResult.Builder resultBuilder = processRow(criteria, searchAttributeStatement, resultSet);
255             String documentId = resultBuilder.getDocument().getDocumentId();
256             if (!resultMap.containsKey(documentId)) {
257                 resultList.add(resultBuilder);
258                 resultMap.put(documentId, resultBuilder);
259                 size++;
260             } else {
261                 // handle duplicate rows with different search data
262                 DocumentSearchResult.Builder previousEntry = resultMap.get(documentId);
263                 handleMultipleDocumentRows(previousEntry, resultBuilder);
264             }
265             resultSetHasNext = resultSet.next();
266         }
267 
268         perfLog.log("Time to read doc search results.", true);
269         // if we have threshold+1 results, then we have more results than we are going to display
270         results.setOverThreshold(resultSetHasNext);
271 
272         LOG.debug("Processed "+size+" document search result rows.");
273         return results;
274     }
275 
276     /**
277      * Handles multiple document rows by collapsing them into the list of document attributes on the existing row.
278      * The two rows must represent the same document.
279      *
280      * @param existingRow the existing row to combine the new row into
281      * @param newRow the new row from which to combine document attributes with the existing row
282      */
283     private void handleMultipleDocumentRows(DocumentSearchResult.Builder existingRow, DocumentSearchResult.Builder newRow) {
284         for (DocumentAttribute.AbstractBuilder<?> newDocumentAttribute : newRow.getDocumentAttributes()) {
285             existingRow.getDocumentAttributes().add(newDocumentAttribute);
286         }
287     }
288 
289     /**
290      * Processes the search result row, returning a DocumentSearchResult
291      * @param criteria the original search criteria
292      * @param searchAttributeStatement statement being used to call the database for queries
293      * @param rs the search result set
294      * @return a DocumentSearchResult representing the current ResultSet row
295      * @throws SQLException
296      */
297     protected DocumentSearchResult.Builder processRow(DocumentSearchCriteria criteria, Statement searchAttributeStatement, ResultSet rs) throws SQLException {
298 
299         String documentId = rs.getString("DOC_HDR_ID");
300         String initiatorPrincipalId = rs.getString("INITR_PRNCPL_ID");
301         String documentTypeName = rs.getString("DOC_TYP_NM");
302         org.kuali.rice.kew.api.doctype.DocumentType documentType =
303                 KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(documentTypeName);
304         if (documentType == null) {
305             throw new IllegalStateException("Failed to locate a document type with the given name: " + documentTypeName);
306         }
307         String documentTypeId = documentType.getId();
308 
309         Document.Builder documentBuilder = Document.Builder.create(documentId, initiatorPrincipalId, documentTypeName, documentTypeId);
310         DocumentSearchResult.Builder resultBuilder = DocumentSearchResult.Builder.create(documentBuilder);
311 
312         String statusCode = rs.getString("DOC_HDR_STAT_CD");
313         Timestamp createTimestamp = rs.getTimestamp("CRTE_DT");
314         String title = rs.getString("TTL");
315         String applicationDocumentStatus = rs.getString("APP_DOC_STAT");
316 
317         documentBuilder.setStatus(DocumentStatus.fromCode(statusCode));
318         documentBuilder.setDateCreated(new DateTime(createTimestamp.getTime()));
319         documentBuilder.setTitle(title);
320         documentBuilder.setApplicationDocumentStatus(applicationDocumentStatus);
321         documentBuilder.setApplicationDocumentStatusDate(new DateTime(rs.getTimestamp("APP_DOC_STAT_MDFN_DT")));
322         documentBuilder.setDateApproved(new DateTime(rs.getTimestamp("APRV_DT")));
323         documentBuilder.setDateFinalized(new DateTime(rs.getTimestamp("FNL_DT")));
324         documentBuilder.setApplicationDocumentId(rs.getString("APP_DOC_ID"));
325         documentBuilder.setDateLastModified(new DateTime(rs.getTimestamp("STAT_MDFN_DT")));
326         documentBuilder.setRoutedByPrincipalId(rs.getString("RTE_PRNCPL_ID"));
327 
328         // TODO - KULRICE-5755 - should probably set as many properties on the document as we can
329         documentBuilder.setDocumentHandlerUrl(rs.getString("DOC_HDLR_URL"));
330 
331         if (isUsingAtLeastOneSearchAttribute(criteria)) {
332             populateDocumentAttributesValues(resultBuilder, searchAttributeStatement);
333         }
334 
335         return resultBuilder;
336     }
337 
338     /**
339      * This method performs searches against the search attribute value tables (see classes implementing
340      * {@link org.kuali.rice.kew.docsearch.SearchableAttributeValue}) to get data to fill in search attribute values on the given resultBuilder parameter
341      *
342      * @param resultBuilder - document search result object getting search attributes added to it
343      * @param searchAttributeStatement - statement being used to call the database for queries
344      * @throws SQLException
345      */
346     public void populateDocumentAttributesValues(DocumentSearchResult.Builder resultBuilder, Statement searchAttributeStatement) throws SQLException {
347         searchAttributeStatement.setFetchSize(50);
348         String documentId = resultBuilder.getDocument().getDocumentId();
349         List<SearchableAttributeValue> attributeValues = DocumentSearchInternalUtils
350                 .getSearchableAttributeValueObjectTypes();
351         PerformanceLogger perfLog = new PerformanceLogger(documentId);
352         for (SearchableAttributeValue searchAttValue : attributeValues) {
353             String attributeSql = "select KEY_CD, VAL from " + searchAttValue.getAttributeTableName() + " where DOC_HDR_ID = '" + documentId + "'";
354             ResultSet attributeResultSet = null;
355             try {
356                 attributeResultSet = searchAttributeStatement.executeQuery(attributeSql);
357                 while (attributeResultSet.next()) {
358                     searchAttValue.setSearchableAttributeKey(attributeResultSet.getString("KEY_CD"));
359                     searchAttValue.setupAttributeValue(attributeResultSet, "VAL");
360                     if ( (!org.apache.commons.lang.StringUtils.isEmpty(searchAttValue.getSearchableAttributeKey())) && (searchAttValue.getSearchableAttributeValue() != null) ) {
361                         DocumentAttribute documentAttribute = searchAttValue.toDocumentAttribute();
362                         resultBuilder.getDocumentAttributes().add(DocumentAttributeFactory.loadContractIntoBuilder(
363                                 documentAttribute));
364                     }
365                 }
366             } finally {
367                 if (attributeResultSet != null) {
368                     try {
369                         attributeResultSet.close();
370                     } catch (Exception e) {
371                         LOG.warn("Could not close searchable attribute result set for class " + searchAttValue.getClass().getName(),e);
372                     }
373                 }
374             }
375         }
376         perfLog.log("Time to execute doc search search attribute queries.", true);
377     }
378 
379     public String generateSearchSql(DocumentSearchCriteria criteria, List<RemotableAttributeField> searchFields) {
380 
381         String docTypeTableAlias   = "DOC1";
382         String docHeaderTableAlias = "DOC_HDR";
383 
384         String sqlPrefix = "Select * from (";
385         String sqlSuffix = ") FINAL_SEARCH order by FINAL_SEARCH.CRTE_DT desc";
386         
387         // the DISTINCT here is important as it filters out duplicate rows which could occur as the result of doc search extension values...
388         StringBuilder selectSQL = new StringBuilder("select DISTINCT("+ docHeaderTableAlias +".DOC_HDR_ID), "
389                                                     + StringUtils.join(new String[] {
390                                                         docHeaderTableAlias + ".INITR_PRNCPL_ID",
391                                                         docHeaderTableAlias + ".DOC_HDR_STAT_CD",
392                                                         docHeaderTableAlias + ".CRTE_DT",
393                                                         docHeaderTableAlias + ".TTL",
394                                                         docHeaderTableAlias + ".APP_DOC_STAT",
395                                                         docHeaderTableAlias + ".STAT_MDFN_DT",
396                                                         docHeaderTableAlias + ".APRV_DT",
397                                                         docHeaderTableAlias + ".FNL_DT",
398                                                         docHeaderTableAlias + ".APP_DOC_ID",
399                                                         docHeaderTableAlias + ".RTE_PRNCPL_ID",
400                                                         docHeaderTableAlias + ".APP_DOC_STAT_MDFN_DT",
401                                                         docTypeTableAlias + ".DOC_TYP_NM",
402                                                         docTypeTableAlias + ".LBL",
403                                                         docTypeTableAlias + ".DOC_HDLR_URL",
404                                                         docTypeTableAlias + ".ACTV_IND"
405                                                     }, ", "));
406         StringBuilder fromSQL = new StringBuilder(" from KREW_DOC_TYP_T "+ docTypeTableAlias +" ");
407         StringBuilder fromSQLForDocHeaderTable = new StringBuilder(", KREW_DOC_HDR_T " + docHeaderTableAlias + " ");
408 
409         StringBuilder whereSQL = new StringBuilder();
410         whereSQL.append(getDocumentIdSql(criteria.getDocumentId(), getGeneratedPredicatePrefix(whereSQL.length()), docHeaderTableAlias));
411         whereSQL.append(getInitiatorSql(criteria.getInitiatorPrincipalName(), getGeneratedPredicatePrefix(whereSQL.length())));
412         whereSQL.append(getAppDocIdSql(criteria.getApplicationDocumentId(), getGeneratedPredicatePrefix(whereSQL.length())));
413         whereSQL.append(getDateCreatedSql(criteria.getDateCreatedFrom(), criteria.getDateCreatedTo(), getGeneratedPredicatePrefix(whereSQL.length())));
414         whereSQL.append(getDateLastModifiedSql(criteria.getDateLastModifiedFrom(), criteria.getDateLastModifiedTo(), getGeneratedPredicatePrefix(whereSQL.length())));
415         whereSQL.append(getDateApprovedSql(criteria.getDateApprovedFrom(), criteria.getDateApprovedTo(), getGeneratedPredicatePrefix(whereSQL.length())));
416         whereSQL.append(getDateFinalizedSql(criteria.getDateFinalizedFrom(), criteria.getDateFinalizedTo(), getGeneratedPredicatePrefix(whereSQL.length())));
417 
418         // flags for the table being added to the FROM class of the sql
419         String principalViewerSql = getViewerSql(criteria.getViewerPrincipalName(), getGeneratedPredicatePrefix(whereSQL.length()));
420         String groupViewerSql = getGroupViewerSql(criteria.getGroupViewerId(), getGeneratedPredicatePrefix(whereSQL.length()));
421         if (StringUtils.isNotBlank(principalViewerSql) || StringUtils.isNotBlank(groupViewerSql)) {
422             whereSQL.append(principalViewerSql);
423             whereSQL.append(groupViewerSql);
424             fromSQL.append(", KREW_ACTN_RQST_T ");
425         }
426 
427         if (!("".equals(getApproverSql(criteria.getApproverPrincipalName(), getGeneratedPredicatePrefix(whereSQL.length()))))) {
428             whereSQL.append(getApproverSql(criteria.getApproverPrincipalName(), getGeneratedPredicatePrefix(whereSQL.length())));
429             fromSQL.append(", KREW_ACTN_TKN_T ");
430         }
431 
432 
433 
434         String docRouteNodeSql = getDocRouteNodeSql(criteria.getDocumentTypeName(), criteria.getRouteNodeName(), criteria.getRouteNodeLookupLogic(), getGeneratedPredicatePrefix(whereSQL.length()));
435         if (StringUtils.isNotBlank(docRouteNodeSql)) {
436             whereSQL.append(docRouteNodeSql);
437             fromSQL.append(", KREW_RTE_NODE_INSTN_T ");
438             fromSQL.append(", KREW_RTE_NODE_T ");
439         }
440 
441         if (!criteria.getDocumentAttributeValues().isEmpty()) {
442             QueryComponent queryComponent = getSearchableAttributeSql(criteria.getDocumentAttributeValues(), searchFields, getGeneratedPredicatePrefix(
443                     whereSQL.length()));
444             selectSQL.append(queryComponent.getSelectSql());
445             fromSQL.append(queryComponent.getFromSql());
446             whereSQL.append(queryComponent.getWhereSql());
447         }
448 
449         whereSQL.append(getDocTypeFullNameWhereSql(criteria, getGeneratedPredicatePrefix(whereSQL.length())));
450         whereSQL.append(getDocTitleSql(criteria.getTitle(), getGeneratedPredicatePrefix(whereSQL.length())));
451         whereSQL.append(getDocumentStatusSql(criteria.getDocumentStatuses(), criteria.getDocumentStatusCategories(), getGeneratedPredicatePrefix(whereSQL.length())));
452         whereSQL.append(getGeneratedPredicatePrefix(whereSQL.length())).append(" DOC_HDR.DOC_TYP_ID = DOC1.DOC_TYP_ID ");
453         fromSQL.append(fromSQLForDocHeaderTable);
454 
455         // App Doc Status Value and Transition clauses
456         String statusTransitionWhereClause = getStatusTransitionDateSql(criteria.getDateApplicationDocumentStatusChangedFrom(), criteria.getDateApplicationDocumentStatusChangedTo(), getGeneratedPredicatePrefix(whereSQL.length()));
457         whereSQL.append(getAppDocStatusSql(criteria.getApplicationDocumentStatus(), getGeneratedPredicatePrefix(whereSQL.length()), statusTransitionWhereClause.length() ));
458         if (statusTransitionWhereClause.length() > 0){
459         	whereSQL.append(statusTransitionWhereClause);
460             whereSQL.append(getGeneratedPredicatePrefix(whereSQL.length())).append(" DOC_HDR.DOC_HDR_ID = STAT_TRAN.DOC_HDR_ID ");
461         	fromSQL.append(", KREW_APP_DOC_STAT_TRAN_T STAT_TRAN ");
462         }
463 
464         String finalizedSql = sqlPrefix + " " + selectSQL.toString() + " " + fromSQL.toString() + " " + whereSQL.toString() + " " + sqlSuffix;
465 
466         LOG.info("*********** SEARCH SQL ***************");
467         LOG.info(finalizedSql);
468         LOG.info("**************************************");
469         return finalizedSql;
470     }
471 
472     public String getDocumentIdSql(String documentId, String whereClausePredicatePrefix, String tableAlias) {
473         if (StringUtils.isBlank(documentId)) {
474             return "";
475         } else {
476         	// Using true for caseInsensitive causes bad performance for MYSQL databases since function indexes cannot be added.
477         	// Due to this, false is passed for caseInsensitive
478             Criteria crit = getSqlBuilder().createCriteria("DOC_HDR_ID", documentId, "KREW_DOC_HDR_T", tableAlias, String.class, false, true);
479             return new StringBuilder(whereClausePredicatePrefix + crit.buildWhere()).toString();
480         }
481     }
482 
483     public String getDocTitleSql(String docTitle, String whereClausePredicatePrefix) {
484         if (StringUtils.isBlank(docTitle)) {
485             return "";
486         } else {
487             // quick and dirty ' replacement that isn't the best but should work for all dbs
488             docTitle = docTitle.trim().replace("\'", "\'\'");
489             SqlBuilder sqlBuild = new SqlBuilder();
490             Criteria crit = new Criteria("KREW_DOC_HDR_T", "DOC_HDR");
491             sqlBuild.addCriteria("TTL", docTitle, String.class, true, true, crit);
492             return new StringBuilder(whereClausePredicatePrefix + crit.buildWhere()).toString();
493         }
494     }
495 
496     // special methods that return the sql needed to complete the search
497     // or nothing if the field was not filled in
498     public String getAppDocIdSql(String appDocId, String whereClausePredicatePrefix) {
499         if (StringUtils.isBlank(appDocId)) {
500             return "";
501         } else {
502             String tableAlias = "DOC_HDR";
503             Criteria crit = getSqlBuilder().createCriteria("APP_DOC_ID", appDocId, "KREW_DOC_HDR_T", tableAlias,String.class);
504             return new StringBuilder(whereClausePredicatePrefix + crit.buildWhere()).toString();
505         }
506     }
507 
508     public String getDateCreatedSql(DateTime fromDateCreated, DateTime toDateCreated, String whereClausePredicatePrefix) {
509         return establishDateString(fromDateCreated, toDateCreated, "KREW_DOC_HDR_T", "DOC_HDR", "CRTE_DT", whereClausePredicatePrefix);
510     }
511 
512     public String getDateApprovedSql(DateTime fromDateApproved, DateTime toDateApproved, String whereClausePredicatePrefix) {
513         return establishDateString(fromDateApproved, toDateApproved, "KREW_DOC_HDR_T", "DOC_HDR", "APRV_DT", whereClausePredicatePrefix);
514     }
515 
516     public String getDateFinalizedSql(DateTime fromDateFinalized, DateTime toDateFinalized, String whereClausePredicatePrefix) {
517         return establishDateString(fromDateFinalized, toDateFinalized, "KREW_DOC_HDR_T", "DOC_HDR", "FNL_DT", whereClausePredicatePrefix);
518     }
519 
520     public String getDateLastModifiedSql(DateTime fromDateLastModified, DateTime toDateLastModified, String whereClausePredicatePrefix) {
521         return establishDateString(fromDateLastModified, toDateLastModified, "KREW_DOC_HDR_T", "DOC_HDR", "STAT_MDFN_DT", whereClausePredicatePrefix);
522     }
523 
524 	public String getStatusTransitionDateSql(DateTime fromStatusTransitionDate, DateTime toStatusTransitionDate, String whereClausePredicatePrefix) {
525         return establishDateString(fromStatusTransitionDate, toStatusTransitionDate, "KREW_DOC_HDR_T", "DOC_HDR", "APP_DOC_STAT_MDFN_DT", whereClausePredicatePrefix);
526     }
527 
528     public String getViewerSql(String viewer, String whereClausePredicatePrefix) {
529         StringBuilder returnSql = new StringBuilder();
530         if (StringUtils.isNotBlank(viewer)) {
531             Map<String, String> m = new HashMap<String, String>();
532             m.put("principalName", viewer);
533 
534             // This will search for people with the ability for the valid operands.
535             List<Person> personList = KimApiServiceLocator.getPersonService().findPeople(m, false);
536             List<String> principalList = new ArrayList<String>();
537 
538             if(CollectionUtils.isEmpty(personList)) {
539             	// findPeople allows for wildcards, but the person must be active.  If no one was found,
540             	// check for an exact inactive user.
541                 PrincipalContract tempPrincipal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(viewer.trim());
542         		if (tempPrincipal != null) {
543                     principalList.add(tempPrincipal.getPrincipalId());
544             	} else {
545                     // they entered something that returned nothing... so we should return nothing
546 
547                     return new StringBuilder(whereClausePredicatePrefix + " 1 = 0 ").toString();
548             	}
549             }
550 
551             for (Person person : personList){
552                 principalList.add(person.getPrincipalId());
553             }
554 
555             Criteria crit = new Criteria("KREW_ACTN_RQST_T", "KREW_ACTN_RQST_T");
556             crit.in("PRNCPL_ID", principalList, String.class);
557             returnSql.append(whereClausePredicatePrefix + "( (DOC_HDR.DOC_HDR_ID = KREW_ACTN_RQST_T.DOC_HDR_ID and " + crit.buildWhere() + " )");
558 
559             Set<String> viewerGroupIds = new TreeSet<String>();
560 
561             if(CollectionUtils.isNotEmpty(principalList)) {
562                 for(String principalId: principalList){
563                     viewerGroupIds.addAll(KimApiServiceLocator.getGroupService().getGroupIdsByPrincipalId(principalId));
564                 }
565             }
566 
567             // Documents routed to users as part of a workgoup should be returned.
568             // Use Chad's escape stuff
569             if (viewerGroupIds != null && !viewerGroupIds.isEmpty()) {
570 
571                 returnSql.append(" or ( " +
572                     "DOC_HDR.DOC_HDR_ID = KREW_ACTN_RQST_T.DOC_HDR_ID " +
573                     "and KREW_ACTN_RQST_T.GRP_ID in (");
574 
575                 boolean first = true;
576                 for (String groupId : viewerGroupIds){
577                     if(!first){
578                         returnSql.append(",");
579                     }
580                     returnSql.append("'").append(groupId).append("'");
581                     first = false;
582                 }
583                 returnSql.append("))");
584             }
585             returnSql.append(")");
586         }
587         return returnSql.toString();
588     }
589 
590     public String getGroupViewerSql(String groupId, String whereClausePredicatePrefix) {
591         String sql = "";
592         if (StringUtils.isNotBlank(groupId)) {
593             sql = whereClausePredicatePrefix + " DOC_HDR.DOC_HDR_ID = KREW_ACTN_RQST_T.DOC_HDR_ID and KREW_ACTN_RQST_T.GRP_ID = '" + groupId + "'";
594         }
595         return sql;
596     }
597 
598     public String getInitiatorSql(String initiatorPrincipalName, String whereClausePredicatePrefix) {
599 
600         if (StringUtils.isBlank(initiatorPrincipalName)) {
601             return "";
602         }
603 
604         String tableAlias = "DOC_HDR";
605 
606         Map<String, String> m = new HashMap<String, String>();
607         m.put("principalName", initiatorPrincipalName);
608 
609         // This will search for people with the ability for the valid operands.
610         List<Person> pList = KimApiServiceLocator.getPersonService().findPeople(m, false);
611         List<String> principalList = new ArrayList<String>();
612 
613         if(pList == null || pList.isEmpty() ){
614        		// findPeople allows for wildcards, but the person must be active.  If no one was found,
615        		// check for an exact inactive user.
616        		PrincipalContract tempPrincipal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(initiatorPrincipalName.trim());
617        		if (tempPrincipal != null) {
618        			principalList.add(tempPrincipal.getPrincipalId());
619        		} else {
620                 // they entered something that returned nothing... so we should return nothing
621                 return new StringBuilder(whereClausePredicatePrefix + " 1 = 0 ").toString();
622         	}
623         }
624 
625         for(Person p: pList){
626             principalList.add(p.getPrincipalId());
627         }
628 
629         Criteria crit = new Criteria("KREW_DOC_HDR_T", tableAlias);
630         crit.in("INITR_PRNCPL_ID", principalList, String.class);
631 
632         return new StringBuilder(whereClausePredicatePrefix + crit.buildWhere()).toString();
633     }
634 
635     public String getApproverSql(String approver, String whereClausePredicatePrefix) {
636         String returnSql = "";
637         if (StringUtils.isNotBlank(approver)) {
638             Map<String, String> m = new HashMap<String, String>();
639             m.put("principalName", approver);
640 
641             // This will search for people with the ability for the valid operands.
642             List<Person> pList = KimApiServiceLocator.getPersonService().findPeople(m, false);
643             List<String> principalList = new ArrayList<String>();
644 
645             if(pList == null || pList.isEmpty() ){
646            		// findPeople allows for wildcards, but the person must be active.  If no one was found,
647            		// check for an exact inactive user.
648                 PrincipalContract tempPrincipal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(approver.trim());
649 
650                 if (tempPrincipal != null) {
651            			principalList.add(tempPrincipal.getPrincipalId());
652                 } else {
653                     // they entered something that returned nothing... so we should return nothing
654                     return new StringBuilder(whereClausePredicatePrefix + " 1 = 0 ").toString();
655                 }
656             }
657 
658             for(Person p: pList){
659                 principalList.add(p.getPrincipalId());
660             }
661 
662             Criteria crit = new Criteria("KREW_ACTN_TKN_T", "KREW_ACTN_TKN_T");
663             crit.in("PRNCPL_ID", principalList, String.class);
664 
665             returnSql = whereClausePredicatePrefix +
666             " DOC_HDR.DOC_HDR_ID = KREW_ACTN_TKN_T.DOC_HDR_ID and upper(KREW_ACTN_TKN_T.ACTN_CD) in ('" +
667             KewApiConstants.ACTION_TAKEN_APPROVED_CD + "','" + KewApiConstants.ACTION_TAKEN_BLANKET_APPROVE_CD + "')" +
668             " and " + crit.buildWhere();
669         }
670         return returnSql;
671     }
672 
673     public String getDocTypeFullNameWhereSql(DocumentSearchCriteria criteria, String whereClausePredicatePrefix) {
674         List<String> documentTypeNamesToSearch = new ArrayList<String>();
675         String primaryDocumentTypeName = criteria.getDocumentTypeName();
676         if (StringUtils.isNotBlank(primaryDocumentTypeName)) {
677             documentTypeNamesToSearch.add(primaryDocumentTypeName);
678         }
679         documentTypeNamesToSearch.addAll(criteria.getAdditionalDocumentTypeNames());
680         StringBuilder returnSql = new StringBuilder("");
681         if (CollectionUtils.isNotEmpty(documentTypeNamesToSearch)) {
682             int index = 0;
683             for (String documentTypeName : documentTypeNamesToSearch) {
684                 if (StringUtils.isNotBlank(documentTypeName)) {
685                     String clause = index++ == 0 ? "" : " or ";
686                     DocumentTypeService docSrv = KEWServiceLocator.getDocumentTypeService();
687                     DocumentType docType = docSrv.findByNameCaseInsensitive(documentTypeName.trim());
688                     if (docType != null) {
689                         if (documentTypeName.contains("*") || documentTypeName.contains("%")) {
690                             addDocumentTypeLikeNameToSearchOn(returnSql, documentTypeName.trim(), clause);
691                         } else {
692                             addDocumentTypeNameToSearchOn(returnSql, documentTypeName.trim(), clause);
693                         }
694                         if (docType.getChildrenDocTypes() != null) {
695                             addChildDocumentTypes(returnSql, docType.getChildrenDocTypes());
696                         }
697                     } else{
698                         addDocumentTypeLikeNameToSearchOn(returnSql, documentTypeName.trim(), clause);
699                     }
700                 }
701             }
702         }
703         if (returnSql.length() > 0) {
704             returnSql.insert(0, "(");
705             returnSql.insert(0, whereClausePredicatePrefix);
706             returnSql.append(")");
707         }
708         return returnSql.toString();
709     }
710 
711     public void addChildDocumentTypes(StringBuilder whereSql, Collection<DocumentType> childDocumentTypes) {
712         for (DocumentType child : childDocumentTypes) {
713             addDocumentTypeNameToSearchOn(whereSql, child.getName());
714             addChildDocumentTypes(whereSql, child.getChildrenDocTypes());
715         }
716     }
717 
718     public void addDocumentTypeNameToSearchOn(StringBuilder whereSql, String documentTypeName) {
719         this.addDocumentTypeNameToSearchOn(whereSql, documentTypeName, " or ");
720     }
721 
722     public void addDocumentTypeNameToSearchOn(StringBuilder whereSql, String documentTypeName, String clause) {
723         whereSql.append(clause).append("upper(DOC1.DOC_TYP_NM) = '" + documentTypeName.toUpperCase() + "'");
724     }
725     public void addDocumentTypeLikeNameToSearchOn(StringBuilder whereSql, String documentTypeName, String clause) {
726         documentTypeName = documentTypeName.replace('*', '%');
727         whereSql.append(clause).append(" upper(DOC1.DOC_TYP_NM) LIKE '" + documentTypeName.toUpperCase() + "'");
728     }
729 
730     public String getDocRouteNodeSql(String documentTypeFullName, String routeNodeName, RouteNodeLookupLogic docRouteLevelLogic, String whereClausePredicatePrefix) {
731         // -1 is the default 'blank' choice from the route node drop down a number is used because the ojb RouteNode object is used to
732         // render the node choices on the form.
733         String returnSql = "";
734         if (StringUtils.isNotBlank(routeNodeName)) {
735             if (docRouteLevelLogic == null) {
736                 docRouteLevelLogic = RouteNodeLookupLogic.EXACTLY;
737             }
738             StringBuilder routeNodeCriteria = new StringBuilder("and " + ROUTE_NODE_TABLE + ".NM ");
739             if (RouteNodeLookupLogic.EXACTLY == docRouteLevelLogic) {
740         		routeNodeCriteria.append("= '" + getDbPlatform().escapeString(routeNodeName) + "' ");
741             } else {
742                 routeNodeCriteria.append("in (");
743                 // below buffer used to facilitate the addition of the string ", " to separate out route node names
744                 StringBuilder routeNodeInCriteria = new StringBuilder();
745                 boolean foundSpecifiedNode = false;
746                 List<RouteNode> routeNodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(getValidDocumentType(documentTypeFullName), true);
747                 for (RouteNode routeNode : routeNodes) {
748                     if (routeNodeName.equals(routeNode.getRouteNodeName())) {
749                         // current node is specified node so we ignore it outside of the boolean below
750                         foundSpecifiedNode = true;
751                         continue;
752                     }
753                     // below logic should be to add the current node to the criteria if we haven't found the specified node
754                     // and the logic qualifier is 'route nodes before specified'... or we have found the specified node and
755                     // the logic qualifier is 'route nodes after specified'
756                     if ( (!foundSpecifiedNode && RouteNodeLookupLogic.BEFORE == docRouteLevelLogic) ||
757                          (foundSpecifiedNode && RouteNodeLookupLogic.AFTER == docRouteLevelLogic) ) {
758                         if (routeNodeInCriteria.length() > 0) {
759                             routeNodeInCriteria.append(", ");
760                         }
761                         routeNodeInCriteria.append("'" + routeNode.getRouteNodeName() + "'");
762                     }
763                 }
764                 if (routeNodeInCriteria.length() > 0) {
765                     routeNodeCriteria.append(routeNodeInCriteria);
766                 } else {
767                     routeNodeCriteria.append("''");
768                 }
769                 routeNodeCriteria.append(") ");
770             }
771             returnSql = whereClausePredicatePrefix + "DOC_HDR.DOC_HDR_ID = " + ROUTE_NODE_INST_TABLE + ".DOC_HDR_ID and " + ROUTE_NODE_INST_TABLE + ".RTE_NODE_ID = " + ROUTE_NODE_TABLE + ".RTE_NODE_ID and " + ROUTE_NODE_INST_TABLE + ".ACTV_IND = 1 " + routeNodeCriteria.toString() + " ";
772         }
773         return returnSql;
774     }
775 
776     public String getDocumentStatusSql(List<DocumentStatus> documentStatuses, List<DocumentStatusCategory> categories, String whereClausePredicatePrefix) {
777         if (CollectionUtils.isEmpty(documentStatuses) && CollectionUtils.isEmpty(categories)) {
778             return whereClausePredicatePrefix + "DOC_HDR.DOC_HDR_STAT_CD != '" + DocumentStatus.INITIATED.getCode() + "'";
779         } else {
780             // include all given document statuses
781             Set<DocumentStatus> statusesToInclude = new HashSet<DocumentStatus>(documentStatuses);
782 
783             // add all statuses from each category
784             for (DocumentStatusCategory category : categories) {
785                 Set<DocumentStatus> categoryStatuses = DocumentStatus.getStatusesForCategory(category);
786                 statusesToInclude.addAll(categoryStatuses);
787             }
788 
789             Set<String> statusCodes = new HashSet<String>();
790             for (DocumentStatus statusToInclude : statusesToInclude) {
791                 statusCodes.add("'" + getDbPlatform().escapeString(statusToInclude.getCode()) + "'");
792             }
793             return whereClausePredicatePrefix + " DOC_HDR.DOC_HDR_STAT_CD in (" + StringUtils.join(statusCodes, ", ") +")";
794         }
795     }
796 
797     /**
798      * This method generates the where clause fragment related to Application Document Status.
799      * If the Status value only is defined, search for the appDocStatus value in the route header.
800      * If either the transition from/to dates are defined, search agains the status transition history.
801      */
802     public String getAppDocStatusSql(String appDocStatus, String whereClausePredicatePrefix, int statusTransitionWhereClauseLength) {
803         if (StringUtils.isBlank(appDocStatus)) {
804             return "";
805         } else {
806         	if (statusTransitionWhereClauseLength > 0){
807         		return whereClausePredicatePrefix + " STAT_TRAN.APP_DOC_STAT_TO = '" + getDbPlatform().escapeString(appDocStatus.trim()) + "'";
808         	}else{
809         		return whereClausePredicatePrefix + " DOC_HDR.APP_DOC_STAT = '" + getDbPlatform().escapeString(appDocStatus.trim()) + "'";
810         	}
811         }
812     }
813 
814     public String getGeneratedPredicatePrefix(int whereClauseSize) {
815         return (whereClauseSize > 0) ? " and " : " where ";
816     }
817 
818     public String establishDateString(DateTime fromDate, DateTime toDate, String tableName, String tableAlias, String colName, String whereStatementClause) {
819 
820         String fromDateValue = null;
821         if (fromDate != null) {
822             fromDateValue = CoreApiServiceLocator.getDateTimeService().toDateString(fromDate.toDate());
823         }
824 
825         String toDateValue = null;
826         if (toDate != null) {
827             toDateValue = CoreApiServiceLocator.getDateTimeService().toDateString(toDate.toDate());
828             toDateValue += " 23:59:59";
829         }
830 
831         String searchValue = null;
832         if (fromDateValue != null && toDateValue != null) {
833             searchValue = fromDateValue + " .. " + toDateValue;
834         } else if (fromDateValue != null) {
835             searchValue = ">= " + fromDateValue;
836         } else if (toDateValue != null) {
837             searchValue = "<= " + toDateValue;
838         } else {
839             return "";
840         }
841 
842         Criteria crit = getSqlBuilder().createCriteria(colName, searchValue, tableName, tableAlias, java.sql.Date.class, true, true);
843         return new StringBuilder(whereStatementClause).append(crit.buildWhere()).toString();
844 
845     }
846 
847     public DatabasePlatform getDbPlatform() {
848         if (dbPlatform == null) {
849             dbPlatform = (DatabasePlatform) GlobalResourceLoader.getService(RiceConstants.DB_PLATFORM);
850         }
851         return dbPlatform;
852     }
853 
854     public SqlBuilder getSqlBuilder() {
855         if(sqlBuilder == null){
856             sqlBuilder = new SqlBuilder();
857             sqlBuilder.setDbPlatform(getDbPlatform());
858             sqlBuilder.setDateTimeService(CoreApiServiceLocator.getDateTimeService());
859         }
860         return this.sqlBuilder;
861     }
862 
863     public void setSqlBuilder(SqlBuilder sqlBuilder) {
864         this.sqlBuilder = sqlBuilder;
865     }
866 
867     /**
868      * A helper method for determining whether any searchable attributes are in use for the search.
869      *
870      * @return True if the search criteria contains at least one searchable attribute or the criteria's doc type name is
871      * non-blank; false otherwise.
872      */
873     protected boolean isUsingAtLeastOneSearchAttribute(DocumentSearchCriteria criteria) {
874         return criteria.getDocumentAttributeValues().size() > 0 || StringUtils.isNotBlank(criteria.getDocumentTypeName());
875     }
876 
877 }