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