View Javadoc

1   /**
2    * Copyright 2010 The Kuali Foundation Licensed under the
3    * Educational Community License, Version 2.0 (the "License"); you may
4    * not use this file except in compliance with the License. You may
5    * obtain a copy of the License at
6    *
7    * http://www.osedu.org/licenses/ECL-2.0
8    *
9    * Unless required by applicable law or agreed to in writing,
10   * software distributed under the License is distributed on an "AS IS"
11   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing
13   * permissions and limitations under the License.
14   */
15  
16  package org.kuali.student.r2.common.dao.impl;
17  
18  import org.apache.log4j.Logger;
19  import org.kuali.student.r2.core.search.dto.QueryParamInfo;
20  import org.kuali.student.r2.core.search.dto.ResultColumnInfo;
21  import org.kuali.student.r2.core.search.dto.SearchTypeInfo;
22  import org.kuali.student.r2.core.search.dto.*;
23  
24  import javax.persistence.EntityManager;
25  import javax.persistence.Query;
26  import java.math.BigDecimal;
27  import java.text.DateFormat;
28  import java.text.ParseException;
29  import java.text.SimpleDateFormat;
30  import java.util.*;
31  
32  public class SearchableCrudDaoImpl {
33  	final Logger LOG = Logger.getLogger(SearchableCrudDaoImpl.class);
34  
35      protected EntityManager em;
36  
37      public SearchableCrudDaoImpl(){
38          super();
39      }
40  
41      public EntityManager getEm() {
42          return em;
43      }
44  
45      public void setEm(EntityManager em) {
46          this.em = em;
47      }
48  	
49  	private static ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
50  		protected DateFormat initialValue() {
51  			return new SimpleDateFormat("EEE MMM dd hh:mm:ss zzz yyyy");
52  		}
53  	};
54  
55  	public SearchResultInfo search(SearchRequestInfo searchRequest,	Map<String, String> queryMap, SearchTypeInfo searchTypeInfo) {
56  		String searchKey = searchRequest.getSearchKey();
57  		
58  		boolean isNative = false;
59  		
60  		//retrieve the SELECT statement from search type definition
61  		String queryString = queryMap.get(searchKey);
62  		String optionalQueryString = "";
63  		if(null == queryString){
64  			LOG.error("No SQL query was found for searchKey:"+searchKey);
65  		}
66  		
67  		if(queryString.toUpperCase().startsWith("NATIVE:")){
68  			queryString = queryString.substring("NATIVE:".length());
69  			isNative = true;
70  		}
71  		
72  		//add in optional
73  		List<SearchParamInfo> searchParamsTemp = new ArrayList<SearchParamInfo>(searchRequest.getParams());
74  		// internalQueryParms is used only internally to know which parameters have to be set in the query
75  		List<SearchParamInfo> internalQueryParms = new ArrayList<SearchParamInfo>(searchRequest.getParams());
76  		for(SearchParamInfo searchParam : searchParamsTemp){
77  			for(QueryParamInfo queryParam:searchTypeInfo.getSearchCriteriaTypeInfo().getQueryParams()){
78  				// check to see if optional param has any values set.
79  				if(queryParam.isOptional()&&queryParam.getKey().equals(searchParam.getKey())
80                          &&searchParam.getValues()!=null&&searchParam.getValues().size()>0&&searchParam.getValues().get(0)!=null){
81  					if(!optionalQueryString.isEmpty()){
82  						optionalQueryString += " AND ";
83  					}
84  					
85  					//if optional query parameter has only a column name then create proper search expression
86  					String condition = queryMap.get(searchParam.getKey());
87  					if(condition==null){
88  						throw new RuntimeException("Optional Param "+searchParam.getKey()+" must have a queryMap definition");
89  					}
90  					if (condition.trim().startsWith("!!")) {
91  					    String substitutionType = condition.trim().substring("!!".length());
92  					    // to detect queryMap value in the form of !!____ ___
93  					    if (condition.contains(" ")) {
94  					        substitutionType = condition.substring("!!".length(), condition.indexOf(" "));
95  					    }
96  					    
97  					    if (substitutionType != null && substitutionType.equals("NUMBER_RANGE")) {
98  					        String realCondition = condition.substring("!!".length() + substitutionType.length()).trim();
99  					        String queryValue = (String)searchParam.getValues().get(0);
100 					        // if the query value is of the form n1 - n2
101 					        if (queryValue != null && queryValue.trim().contains("-")) {
102 					            StringTokenizer strTokenizer = new StringTokenizer(queryValue.trim(),"-");
103 					            if (strTokenizer.hasMoreElements()) {
104 					                String strNum1 = strTokenizer.nextToken().trim();
105 					                String strNum2 = strTokenizer.nextToken().trim();
106 					                optionalQueryString += 
107 					                    realCondition +
108 					                    " BETWEEN " + "'" + strNum1 + "'" + " AND " + "'" + strNum2 + "'";
109 					                internalQueryParms.remove(searchParam);
110 					            }
111 					        } else {
112 					            // the value is just one number
113 					            optionalQueryString += realCondition + " = '" + queryValue + "'";
114 					            internalQueryParms.remove(searchParam);
115 					        }
116 					    }
117 					} else if (condition.trim().contains(":")) {
118 					    //this parameter is not entered by end user but rather it is set with a default context value	
119 					    String dataType = queryParam.getFieldDescriptor().getDataType();
120 					    if ((dataType != null) && "boolean".equals(dataType)) {
121 					        optionalQueryString += queryMap.get(searchParam.getKey()).replace(":" + searchParam.getKey().replace(".", "_"), searchParam.getValues().get(0));
122 					        internalQueryParms.remove(searchParam);
123 					    } else {					    
124 					        optionalQueryString += queryMap.get(searchParam.getKey());
125 					    }						
126 					} else {
127 						//comparison should be case insensitive and should include wild card such that we match beginning of a text 
128 						//and each word within text
129 						//FIXME SQL injection can occur here - or NOT if we need to assemble SQL to cover various ways one can compare criteria to a text
130 						optionalQueryString += 
131                                 "(LOWER(" + queryMap.get(searchParam.getKey()) + ") LIKE LOWER(:"
132                                         + searchParam.getKey().replace(".", "_") + ") || '%' OR " +
133                                         "LOWER(" + queryMap.get(searchParam.getKey()) + ") LIKE '% ' || LOWER(:"
134                                         + searchParam.getKey().replace(".", "_") + ") || '%')";
135 					}
136 				}
137 			}
138 		}
139 		
140 		//Add in the where clause or And clause if needed for the optional criteria
141 		if(!optionalQueryString.isEmpty()){
142 			if(!queryString.toUpperCase().contains(" WHERE ")){
143 				queryString += " WHERE ";
144 			}
145 			else {
146 				queryString += " AND ";
147 			}
148 		}
149 		
150 		//Do ordering
151 		String orderByClause = "";		
152 		if(!queryString.toUpperCase().contains("ORDER BY")&&searchRequest.getSortColumn()!=null){
153 			//make sure the sort column is a real result column
154 			int i = 0;
155 			
156 			//Get an array of the jpql results
157             String lowercaseQueryString = queryString.toLowerCase();
158 			int selectIndex = lowercaseQueryString.indexOf("select")+"select".length();
159             int distinctIndex = lowercaseQueryString.indexOf("distinct")+"distinct".length();//Filter out the "distinct"
160 			int fromIndex = lowercaseQueryString.indexOf(" from ");
161             if(selectIndex < distinctIndex && distinctIndex < fromIndex){
162                 selectIndex = distinctIndex;
163             }
164 			
165 			if (selectIndex >= 0 && fromIndex > selectIndex){
166 				String[] jpqlResultColumns = queryString.substring(selectIndex, fromIndex).replaceAll("\\s", "").split(",");
167 				for(ResultColumnInfo results : searchTypeInfo.getSearchResultTypeInfo().getResultColumns()){
168 					if(results.getKey().equals(searchRequest.getSortColumn())){
169                         if(results.getDataType()!=null && "string".equals(results.getDataType().toLowerCase())){
170                         	orderByClause = " ORDER BY LOWER(" + jpqlResultColumns[i] + ") ";
171                         }else{
172                         	//Don't sort dates or numbers in alphabetic order or weirdness happens
173                         	orderByClause = " ORDER BY " + jpqlResultColumns[i] + " ";
174                         }
175 						if(searchRequest.getSortDirection()!=null && searchRequest.getSortDirection()==SortDirection.DESC){
176 							orderByClause += "DESC ";
177 						}else{
178 							orderByClause += "ASC ";
179 						}
180 					}
181 					i++;
182 				}
183 			}			
184 		}
185 		
186 		//Create the query
187 		String finalQueryString = queryString + optionalQueryString + orderByClause;
188 		
189 		//remove special characters and extra spaces
190 		//finalQueryString = finalQueryString.replaceAll("[\n\r\t]", " ");
191 		//finalQueryString = finalQueryString.replaceAll("\\s+", " ");
192 		
193 		Query query;
194 		if(isNative){
195 			LOG.info("Native Query:"+finalQueryString);
196 			query = em.createNativeQuery(finalQueryString);
197 		}else{
198 			LOG.info("JPQL Query:"+finalQueryString);
199 			query = em.createQuery(finalQueryString);
200 		}
201 		
202 		//Set the pagination information (eg. only return 25 rows starting at row 100)
203 		if(searchRequest.getStartAt()!=null){
204 			query.setFirstResult(searchRequest.getStartAt().intValue());
205 		}
206 		if(searchRequest.getMaxResults()!=null){
207 			query.setMaxResults(searchRequest.getMaxResults().intValue());
208 		}
209 		
210 		//replace all the "." notation with "_" since the "."s in the ids of the queries will cause problems with the jpql  
211 		for (SearchParamInfo searchParam : internalQueryParms) {
212 			// check to see if optional param has any values set.
213 			if (searchParam.getValues() != null &&
214                     searchParam.getValues().size() > 0 &&
215                     searchParam.getValues().get(0) != null) {
216 			    List<QueryParamInfo> queryParams = searchTypeInfo.getSearchCriteriaTypeInfo().getQueryParams();
217 			    String paramDataType = null;
218 			    if (queryParams != null) {
219 			        for (QueryParamInfo queryParam : queryParams) {
220 			            if (queryParam.getKey() != null && queryParam.getKey().equals(searchParam.getKey())) {
221 			                paramDataType = queryParam.getFieldDescriptor().getDataType();
222 			            }
223 			        }
224 			    }
225 			    
226                 Object queryParamValue = null;
227 			    if ("date".equals(paramDataType)) {
228 			        try {
229                         queryParamValue = df.get().parse((String)searchParam.getValues().get(0));
230                     } catch (ParseException e) {
231                         throw new RuntimeException("Failed to parse date value " + searchParam.getValues().get(0),e);
232                     }
233 			    } else if ("long".equals(paramDataType)){
234 			    	try{
235 			    		List<Long> longList = new ArrayList<Long>();
236 			    		if(searchParam.getValues()!=null){
237 			    			for(String value:searchParam.getValues()){
238 			    				longList.add(Long.parseLong(value));
239 			    			}
240 			    		}
241 				      	queryParamValue = longList;
242 		            } catch (NumberFormatException e) {
243 		                throw new RuntimeException("Failed to parse long value " + searchParam.getValues(),e);
244 		            }
245 
246 			    } else if ("int".equals(paramDataType)){
247                     try{
248                         List<Integer> intList = new ArrayList<Integer>();
249                         if(searchParam.getValues()!=null){
250                             for(String value:searchParam.getValues()){
251                                 intList.add(Integer.parseInt(value));
252                             }
253                         }
254                         queryParamValue = intList;
255                     } catch (NumberFormatException e) {
256                         throw new RuntimeException("Failed to parse int value " + searchParam.getValues(),e);
257                     }
258 
259                 }  else {
260 			        queryParamValue = searchParam.getValues();
261 			    }
262 			    //Needed to get around Hibernate not supporting IN(:var) where var is null or an empty collection
263 			    if((queryParamValue==null||queryParamValue instanceof Collection && ((Collection<?>)queryParamValue).isEmpty())&&"list".equals(paramDataType)){
264 			    	queryParamValue = "";
265 			    }
266 			    query.setParameter(searchParam.getKey().replace(".", "_"), queryParamValue);
267 			}
268 		}
269 
270 		// Turn into results
271 		List<SearchResultRowInfo> results = convertToResults(query.getResultList(),searchTypeInfo);
272 
273 		SearchResultInfo searchResult = new SearchResultInfo();
274 		searchResult.setRows(results);
275 		searchResult.setSortColumn(searchRequest.getSortColumn());
276 		searchResult.setSortDirection(searchRequest.getSortDirection());
277 		searchResult.setStartAt(searchRequest.getStartAt());
278 		if(searchRequest.getNeededTotalResults()!=null && searchRequest.getNeededTotalResults()){
279 			//Get count of total rows if needed
280             String regex = "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt]\\s+([^,\\s]+)(.|[\r\n])*?\\s+[Ff][Rr][Oo][Mm]\\s+";
281             String replacement = "SELECT COUNT(DISTINCT $1) FROM ";
282             queryString = queryString.replaceAll("([Dd][Ii][Ss][Tt][Ii][Nn][Cc][Tt])", "");
283 			String countQueryString = (queryString + optionalQueryString).replaceFirst(regex, replacement);
284 
285 			LOG.info("Executing query: "+countQueryString);
286 			Query countQuery;
287 			if(isNative){
288 				countQuery = em.createNativeQuery(countQueryString);
289 			}else{
290 				countQuery = em.createQuery(countQueryString);
291 			}
292 			for (SearchParamInfo searchParam : internalQueryParms) {
293 			    // There is a bug here that happens with you pass a list of states in for lu_queryParam_luOptionalState
294 			    // When you pass in ('Approved' , 'Active' , 'Retired') the code below only grabbed the first value (it was
295 			    // hard coded with get(0). Thus, it would only count 'Approved' courses.  However, at UMD, there aren't even
296 			    // approved courses in the KSLU_CLU table (only 'Active') so we were getting an empty list back.
297 			    // Printout of searchParam.getValues():  [Approved, Active, Retired, null, null, null, null, null, null, null]
298 			    // You can see broken code only grabs the first value in the array using searchParam.getValues().get(0).
299 			    // select * from kslu_clu where st  IN ( 'Approved','Active', 'Retired' )
300 			    // Fix was to pass in the entire list of values when getValues() > 1
301 			  
302 			    if ( searchParam.getValues().size() == 1){
303 			        countQuery.setParameter(searchParam.getKey().replace(".", "_"), searchParam.getValues().get(0));
304 			    }
305 			    else {
306 			        countQuery.setParameter(searchParam.getKey().replace(".", "_"), searchParam.getValues());  
307 			    } 		
308 			}
309             Integer totalRecords = 0;
310             Object resultObject = countQuery.getSingleResult();
311             if (resultObject instanceof BigDecimal) {
312                 totalRecords = ((BigDecimal) resultObject).intValue();
313             } else if (resultObject instanceof Long) {
314                 totalRecords = ((Long) resultObject).intValue();
315             }
316             searchResult.setTotalResults(totalRecords);
317 		}
318 
319 		return searchResult;
320 	}
321 	
322 	private List<SearchResultRowInfo> convertToResults(List<?> queryResults,
323 			SearchTypeInfo searchTypeInfo) {
324 		List<SearchResultRowInfo> results = new ArrayList<SearchResultRowInfo>();
325 
326 		if(queryResults!=null){
327 			//Copy the query results to a Result object
328 			for(Object queryResult:queryResults){
329 				SearchResultRowInfo result = new SearchResultRowInfo();
330 				int i=0;
331 				for (ResultColumnInfo resultColumn : searchTypeInfo.getSearchResultTypeInfo().getResultColumns()) {
332 			
333 					SearchResultCellInfo resultCell = new SearchResultCellInfo();
334 					resultCell.setKey(resultColumn.getKey());
335 					
336 					try {
337 						Object queryResultCell = null;
338 						if(queryResult.getClass().isArray()){
339 							queryResultCell = ((Object[])queryResult)[i];
340 							
341 						}else{
342 							queryResultCell = queryResult;
343 						}
344 						
345 						if (queryResultCell != null) {
346 							resultCell.setValue(queryResultCell.toString());							
347 						}
348 					
349 					} catch (Exception e) {
350 						throw new RuntimeException("Error copying results from " + queryResult.toString(),e);
351 					}
352 					
353 					result.getCells().add(resultCell);
354 					i++;
355 				}
356 				results.add(result);
357 			}
358 		}
359 		return results;
360 	}
361 
362 }