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