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