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.common.search.service.impl;
17  
18  import java.text.SimpleDateFormat;
19  import java.util.ArrayList;
20  import java.util.Date;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  
25  import org.kuali.student.common.search.dto.CrossSearchTypeInfo;
26  import org.kuali.student.common.search.dto.JoinComparisonInfo;
27  import org.kuali.student.common.search.dto.JoinComparisonInfo.ComparisonType;
28  import org.kuali.student.common.search.dto.JoinCriteriaInfo;
29  import org.kuali.student.common.search.dto.JoinCriteriaInfo.JoinType;
30  import org.kuali.student.common.search.dto.JoinResultMappingInfo;
31  import org.kuali.student.common.search.dto.SearchParam;
32  import org.kuali.student.common.search.dto.SearchRequest;
33  import org.kuali.student.common.search.dto.SearchResult;
34  import org.kuali.student.common.search.dto.SearchResultCell;
35  import org.kuali.student.common.search.dto.SearchResultRow;
36  import org.kuali.student.common.search.dto.SubSearchInfo;
37  import org.kuali.student.common.search.dto.SubSearchParamMappingInfo;
38  import org.kuali.student.common.search.service.SearchDispatcher;
39  
40  /**
41   * This still needs a few things
42   * 1 - no search meta(sort, pagination) is implemented
43   * 2 - a way to do subselects should be implemented to reduce the processing and sheer size of the unions
44   * (for example if searching for LO and the related CLU by the LO description, we need to match ALL CLUs 
45   * with just the LOs that match, meaning if we had 1000 clus, and 10 LOs we would be comparing 10000 results)
46   * 
47   *
48   */
49  /**
50   * @author Daniel Epstein
51   *
52   */
53  public class CrossSearchManager {
54  	private SearchDispatcher searchDispatcher;
55  
56  	public SearchResult doCrossSearch(SearchRequest searchRequest, CrossSearchTypeInfo crossSearchType) {
57  		SearchResult searchResult = new SearchResult();
58  		
59  		Map<String,SearchResult> subSearchResults = new HashMap<String,SearchResult>();
60  		
61  		//First perform all the subsearches
62  		for(SubSearchInfo subSearch:crossSearchType.getSubSearches()){
63  			//Map the parameters to the subsearch
64  			SearchRequest subSearchRequest = new SearchRequest();
65  			
66  			subSearchRequest.setSearchKey(subSearch.getSearchkey());
67  			subSearchRequest.setParams(new ArrayList<SearchParam>());
68              subSearchRequest.setSortColumn(searchRequest.getSortColumn());
69              subSearchRequest.setSortDirection(searchRequest.getSortDirection());
70  			
71  			//For each param mapping, map the paramvalue from the cross search to the sub search
72  			for(SubSearchParamMappingInfo paramMapping:subSearch.getSubSearchParamMappings()){
73  				for(SearchParam crossSearchParam:searchRequest.getParams()){
74  					if(paramMapping.getCrossSearchParam().equals(crossSearchParam.getKey())){
75  						SearchParam subSearchParam = new SearchParam();
76  						subSearchParam.setKey(paramMapping.getSubSearchParam());
77  						Object paramValue = crossSearchParam.getValue();
78  						if(paramValue instanceof String){
79  							subSearchParam.setValue((String)paramValue);
80  						}else if(paramValue instanceof List<?>){
81  							subSearchParam.setValue((List<String>)paramValue);
82  						}
83  						subSearchRequest.getParams().add(subSearchParam);
84  					}
85  				}
86  			}
87  			SearchResult subSearchResult = searchDispatcher.dispatchSearch(subSearchRequest);
88  			subSearchResults.put(subSearch.getKey(), subSearchResult);
89  		}
90  		
91  		//merge the subsearches together using the join rules
92  		if(crossSearchType.getJoinCriteria().getComparisons().isEmpty()){
93  			//If the root join has no criteria then do a simple union of rows
94  			for(Map.Entry<String,SearchResult> subSearchResult:subSearchResults.entrySet()){
95  				for(SearchResultRow row:subSearchResult.getValue().getRows()){
96  					SearchResultRow mappedResult = mapResultRow(subSearchResult.getKey(),row,crossSearchType);
97  					searchResult.getRows().add(mappedResult);
98  				}
99  			}
100 		}else{
101 			//merge the subsearches together using the join rules (this is in o^2 time which is bad)
102 			List <Map<String,SearchResultRow>> allPermutations = unionOfAllRows(subSearchResults);
103 	
104 			for(Map<String,SearchResultRow> permutation:allPermutations){
105 				if(meetsCriteria(permutation,crossSearchType,crossSearchType.getJoinCriteria())){
106 					SearchResultRow mappedResult = mapResultRow(permutation,crossSearchType);
107 					searchResult.getRows().add(mappedResult);
108 				}
109 			}
110 		}
111 		return metaFilter(searchResult,searchRequest);
112 	}
113 	
114 	
115 	
116 	
117 	/**
118 	 * @param searchResult
119 	 * @param searchRequest
120 	 * @return a sorted and paginated result
121 	 */
122 	private SearchResult metaFilter(SearchResult searchResult,
123 		SearchRequest searchRequest) {
124 		
125         searchResult.setTotalResults(searchResult.getRows().size());
126 
127 		searchResult.sortRows();		
128 		
129 		//Paginate if we need to
130 		if(searchRequest.getMaxResults()!=null){
131 			int fromIndex=0;
132 			if(searchRequest.getStartAt()!=null){
133 				fromIndex=searchRequest.getStartAt();
134 			}
135 			int toIndex = fromIndex+searchRequest.getMaxResults();
136 			SearchResult pagedResult = new SearchResult();
137 			for (int i=fromIndex; i <= toIndex; i++) {
138 				if (!(searchResult.getRows().size() < i+1)) {
139 					pagedResult.getRows().add(searchResult.getRows().get(i));
140 				}
141 			}
142             pagedResult.setTotalResults(searchResult.getRows().size());
143 			searchResult = pagedResult;
144 		}
145 		return searchResult;
146 	}
147 
148 	/**
149 	 * Maps results from multiple searches into a single result row
150 	 *
151 	 * @param permutation
152 	 * @param crossSearchType
153 	 * @return a mapped SearchResultRow
154 	 */
155 	private SearchResultRow mapResultRow(
156 			Map<String, SearchResultRow> permutation,
157 			CrossSearchTypeInfo crossSearchType) {
158 		//FIXME this is pretty inefficient to loop through everything... a map structure for the cells might be better
159 		SearchResultRow resultRow = new SearchResultRow();
160 		for(JoinResultMappingInfo resultMapping: crossSearchType.getJoinResultMappings()){
161 			for(SearchResultCell cell: permutation.get(resultMapping.getSubSearchKey()).getCells()){
162 				if(resultMapping.getSubSearchResultParam().equals(cell.getKey())){
163 					SearchResultCell mappedCell = new SearchResultCell();
164 					mappedCell.setKey(resultMapping.getResultParam());
165 					mappedCell.setValue(cell.getValue());
166 					resultRow.getCells().add(mappedCell);
167 					break;//FIXME breaks are bad... but there is no map in the cells
168 				}
169 			}
170 		}
171 		return resultRow;
172 		
173 	}
174 
175 	private SearchResultRow mapResultRow(
176 			String subSearchKey, SearchResultRow row,
177 			CrossSearchTypeInfo crossSearchType) {
178 		SearchResultRow resultRow = new SearchResultRow();
179 		
180 		for(JoinResultMappingInfo resultMapping: crossSearchType.getJoinResultMappings()){
181 			if(subSearchKey.equals(resultMapping.getSubSearchKey())){
182 				for(SearchResultCell cell: row.getCells()){
183 					if(resultMapping.getSubSearchResultParam().equals(cell.getKey())){
184 						SearchResultCell mappedCell = new SearchResultCell();
185 						mappedCell.setKey(resultMapping.getResultParam());
186 						mappedCell.setValue(cell.getValue());
187 						resultRow.getCells().add(mappedCell);
188 						break;//FIXME breaks are bad... but there is no map in the cells
189 					}
190 				}
191 			}
192 		}
193 		return resultRow;
194 	}
195 	/**
196 	 * Checks each comparison of the join criteria and recursively checks through nested criteria.  
197 	 * Short circuits for false 'AND' joins and true 'OR' joins
198 	 * @param permutation
199 	 * @param crossSearchType
200 	 * @param joinCriteria
201 	 * @return whether the criteria is met
202 	 */
203 	private boolean meetsCriteria(Map<String, SearchResultRow> permutation,
204 			CrossSearchTypeInfo crossSearchType, JoinCriteriaInfo joinCriteria){
205 
206 		JoinType joinType = joinCriteria.getJoinType();
207 		
208 		//Check actual comparisons
209 		for(JoinComparisonInfo comparison:joinCriteria.getComparisons()){
210 			SearchResultRow leftResultRow =  permutation.get(comparison.getLeftHandSide().getSubSearchKey());
211 			String leftResultValue = null;
212 			if(leftResultRow!=null){
213 				for(SearchResultCell cell: leftResultRow.getCells()){
214 					if(comparison.getLeftHandSide().getParam().equals(cell.getKey())){
215 						leftResultValue = cell.getValue();
216 						break;//FIXME breaks are bad... but there is no map in the cells
217 					}
218 				}
219 			}
220 			
221 			SearchResultRow rightResultRow =  permutation.get(comparison.getRightHandSide().getSubSearchKey());
222 			String rightResultValue = null;
223 			if(rightResultRow!=null){
224 				for(SearchResultCell cell: rightResultRow.getCells()){
225 					if(comparison.getRightHandSide().getParam().equals(cell.getKey())){
226 						rightResultValue = cell.getValue();
227 						break;//FIXME breaks are bad... but there is no map in the cells
228 					}
229 				}
230 			}			
231 			
232 			//Get the compare type for the 
233 			//TODO get the types for the params!
234 			if(leftResultValue==null||rightResultValue==null){
235 				int i=0;i++;
236 			}
237 			if(compare(null, leftResultValue,rightResultValue,comparison.getType())){
238 				if(JoinType.OR.equals(joinType)){
239 					return true;
240 				}
241 			}else{
242 				if(JoinType.AND.equals(joinType)){
243 					return false;
244 				}
245 			}
246 		}
247 		
248 		//Check all subcriteria next
249 		for(JoinCriteriaInfo subCriteria: joinCriteria.getJoinCriteria()){
250 			if(meetsCriteria(permutation, crossSearchType, subCriteria)){
251 				if(JoinType.OR.equals(joinType)){
252 					return true;
253 				}
254 			}else{
255 				if(JoinType.AND.equals(joinType)){
256 					return false;
257 				}
258 			}
259 		}
260 		
261 		if(JoinType.AND.equals(joinType)){
262 			return true;
263 		}
264 		if(JoinType.OR.equals(joinType)){
265 			return false;
266 		}
267 		
268 		return false;
269 	}
270 
271 	/**
272 	 * @param searchResults
273 	 * @return a list of all possible combinations of rows
274 	 */
275 	private List <Map<String,SearchResultRow>> unionOfAllRows(Map<String, SearchResult> searchResults){
276 		List <Map<String,SearchResultRow>> r = new ArrayList<Map<String,SearchResultRow>>();
277 		for(Map.Entry<String,SearchResult> x:searchResults.entrySet()){
278 			List<Map<String,SearchResultRow>> t = new ArrayList<Map<String,SearchResultRow>>();
279 			if(x.getValue()!=null&&x.getValue().getRows()!=null){
280 				for(SearchResultRow y:x.getValue().getRows()){
281 					for(Map<String,SearchResultRow> i:r){
282 						Map<String,SearchResultRow> unions =  new HashMap<String,SearchResultRow>();
283 						unions.putAll(i);
284 						unions.put(x.getKey(), y);
285 						t.add(unions);
286 					}
287 					if(r.size()==0){
288 						Map<String,SearchResultRow> unions  =  new HashMap<String,SearchResultRow>();
289 						unions.put(x.getKey(), y);
290 						t.add(unions);
291 					}
292 				}
293 			}
294 			r = t;
295 		}
296 		return r;
297 	}	
298 	
299 	private enum DataType{STRING,INT,BOOLEAN,DATE}
300 	
301 
302 
303 	private boolean compare(DataType dataType, String left, String right,
304 			ComparisonType type ){
305 		//FIXME needs a handle to the result params data types here
306 		try{
307 			Integer leftInteger = Integer.parseInt(left);
308 			Integer rightInteger = Integer.parseInt(right);
309 			return compareInt(leftInteger,rightInteger,type);
310 		}catch(Exception e){
311 		}
312 		try{
313 			if(("true".equals(left.toLowerCase())||"false".equals(left.toLowerCase()))&&
314 			   ("true".equals(right.toLowerCase())||"false".equals(right.toLowerCase()))){
315 				Boolean leftBoolean = Boolean.parseBoolean(left);
316 				Boolean rightBoolean = Boolean.parseBoolean(right);
317 				return compareBoolean(leftBoolean,rightBoolean,type);
318 			}
319 		}catch(Exception e){
320 		}
321 		try{
322 			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
323 			Date leftDate = df.parse(left);
324 			Date rightDate = df.parse(right);
325 			return compareDate(leftDate,rightDate,type);
326 		}catch(Exception e){
327 		}
328 		return compareString(left,right,type);
329 //		switch(dataType){
330 //			case BOOLEAN:
331 //				Boolean leftBoolean = new Boolean(left);
332 //				Boolean rightBoolean = new Boolean(right);
333 //				return compareBoolean(leftBoolean,rightBoolean,type);
334 //			case DATE:
335 //				SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
336 //				Date leftDate = df.parse(left);
337 //				Date rightDate = df.parse(right);
338 //				return compareDate(leftDate,rightDate,type);
339 //			case INT:
340 //				Integer leftInteger = Integer.getInteger(left);
341 //				Integer rightInteger = Integer.getInteger(right);
342 //				return compareInt(leftInteger,rightInteger,type);
343 //			case STRING:
344 //				return compareString(left,right,type);
345 //		}
346 //		return false;
347 	}
348 	
349 	private boolean compareString(String left, String right, ComparisonType type) {
350 		switch(type){
351 		case EQUALS:
352 			return left.equals(right);
353 		case GREATERTHAN:
354 			return left.compareTo(right) > 0;
355 		case GREATERTHANEQUALS:
356 			return left.compareTo(right) >= 0;
357 		case LESSTHAN:
358 			return left.compareTo(right) < 0;
359 		case LESSTHANEQUALS:
360 			return left.compareTo(right) <= 0;
361 		case NOTEQUALS:
362 			return !left.equals(right);
363 		}
364 		return false;
365 	}
366 
367 	private boolean compareInt(Integer left, Integer right, ComparisonType type) {
368 		switch(type){
369 		case EQUALS:
370 			return left.equals(right);
371 		case GREATERTHAN:
372 			return left.compareTo(right) > 0;
373 		case GREATERTHANEQUALS:
374 			return left.compareTo(right) >= 0;
375 		case LESSTHAN:
376 			return left.compareTo(right) < 0;
377 		case LESSTHANEQUALS:
378 			return left.compareTo(right) <= 0;
379 		case NOTEQUALS:
380 			return !left.equals(right);
381 		}
382 		return false;
383 	}
384 
385 	private boolean compareDate(Date left, Date right, ComparisonType type) {
386 		switch(type){
387 		case EQUALS:
388 			return left.equals(right);
389 		case GREATERTHAN:
390 			return left.compareTo(right) > 0;
391 		case GREATERTHANEQUALS:
392 			return left.compareTo(right) >= 0;
393 		case LESSTHAN:
394 			return left.compareTo(right) < 0;
395 		case LESSTHANEQUALS:
396 			return left.compareTo(right) <= 0;
397 		case NOTEQUALS:
398 			return !left.equals(right);
399 		}
400 		return false;
401 	}
402 
403 	private boolean compareBoolean(Boolean left, Boolean right,
404 			ComparisonType type) {
405 		switch(type){
406 		case EQUALS:
407 			return left.equals(right);
408 		case GREATERTHAN:
409 			return left.compareTo(right) > 0;
410 		case GREATERTHANEQUALS:
411 			return left.compareTo(right) >= 0;
412 		case LESSTHAN:
413 			return left.compareTo(right) < 0;
414 		case LESSTHANEQUALS:
415 			return left.compareTo(right) <= 0;
416 		case NOTEQUALS:
417 			return !left.equals(right);
418 		}
419 		return false;
420 	}
421 	public void setSearchDispatcher(SearchDispatcher searchDispatcher) {
422 		this.searchDispatcher = searchDispatcher;
423 	}
424 
425 	public SearchDispatcher getSearchDispatcher() {
426 		return searchDispatcher;
427 	}
428 		
429 }