001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kew.impl.document.search;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.joda.time.DateTime;
021    import org.kuali.rice.core.api.CoreApiServiceLocator;
022    import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
023    import org.kuali.rice.core.api.uif.RemotableAttributeError;
024    import org.kuali.rice.core.api.uif.RemotableAttributeField;
025    import org.kuali.rice.core.api.util.RiceConstants;
026    import org.kuali.rice.core.api.util.RiceKeyConstants;
027    import org.kuali.rice.core.framework.persistence.jdbc.sql.Criteria;
028    import org.kuali.rice.core.framework.persistence.jdbc.sql.SqlBuilder;
029    import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
030    import org.kuali.rice.kew.api.KewApiConstants;
031    import org.kuali.rice.kew.api.KewApiServiceLocator;
032    import org.kuali.rice.kew.api.document.Document;
033    import org.kuali.rice.kew.api.document.DocumentStatus;
034    import org.kuali.rice.kew.api.document.DocumentStatusCategory;
035    import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
036    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
037    import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
038    import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
039    import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
040    import org.kuali.rice.kew.api.document.search.RouteNodeLookupLogic;
041    import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
042    import org.kuali.rice.kew.docsearch.QueryComponent;
043    import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
044    import org.kuali.rice.kew.doctype.bo.DocumentType;
045    import org.kuali.rice.kew.doctype.service.DocumentTypeService;
046    import org.kuali.rice.kew.engine.node.RouteNode;
047    import org.kuali.rice.kew.service.KEWServiceLocator;
048    import org.kuali.rice.kew.util.PerformanceLogger;
049    import org.kuali.rice.kim.api.identity.Person;
050    import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
051    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
052    import org.kuali.rice.krad.util.GlobalVariables;
053    import org.kuali.rice.krad.util.MessageMap;
054    
055    import java.sql.ResultSet;
056    import java.sql.SQLException;
057    import java.sql.Statement;
058    import java.sql.Timestamp;
059    import java.util.ArrayList;
060    import java.util.Collection;
061    import java.util.Collections;
062    import java.util.HashMap;
063    import java.util.HashSet;
064    import java.util.List;
065    import java.util.Map;
066    import java.util.Set;
067    import java.util.TreeSet;
068    
069    
070    /**
071     * Reference implementation of the {@code DocumentSearchGenerator}.
072     *
073     * @author Kuali Rice Team (rice.collab@kuali.org)
074     */
075    public class DocumentSearchGeneratorImpl implements DocumentSearchGenerator {
076    
077        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSearchGeneratorImpl.class);
078    
079        private static final String ROUTE_NODE_TABLE = "KREW_RTE_NODE_T";
080        private static final String ROUTE_NODE_INST_TABLE = "KREW_RTE_NODE_INSTN_T";
081        private static final String DATABASE_WILDCARD_CHARACTER_STRING = "%";
082        private static final char DATABASE_WILDCARD_CHARACTER = DATABASE_WILDCARD_CHARACTER_STRING.toCharArray()[0];
083    
084        private DatabasePlatform dbPlatform;
085        private MessageMap messageMap;
086    
087        private SqlBuilder sqlBuilder = null;
088    
089        @Override
090        public DocumentSearchCriteria clearSearch(DocumentSearchCriteria criteria) {
091            return DocumentSearchCriteria.Builder.create().build();
092        }
093    
094        public DocumentType getValidDocumentType(String documentTypeFullName) {
095            if (!org.apache.commons.lang.StringUtils.isEmpty(documentTypeFullName)) {
096                DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeFullName);
097                if (documentType == null) {
098                    throw new RuntimeException("No Valid Document Type Found for document type name '" + documentTypeFullName + "'");
099                }
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    }