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.class1.search;
17  
18  import org.kuali.student.r2.common.dto.ContextInfo;
19  import org.kuali.student.r2.common.exceptions.MissingParameterException;
20  import org.kuali.student.r2.common.exceptions.OperationFailedException;
21  import org.kuali.student.r2.common.exceptions.PermissionDeniedException;
22  import org.kuali.student.r2.common.util.date.DateFormatters;
23  import org.kuali.student.r2.core.search.dto.CrossSearchTypeInfo;
24  import org.kuali.student.r2.core.search.dto.JoinComparisonInfo;
25  import org.kuali.student.r2.core.search.dto.JoinComparisonInfo.ComparisonType;
26  import org.kuali.student.r2.core.search.dto.JoinCriteriaInfo;
27  import org.kuali.student.r2.core.search.dto.JoinCriteriaInfo.JoinType;
28  import org.kuali.student.r2.core.search.dto.JoinResultMappingInfo;
29  import org.kuali.student.r2.core.search.dto.SearchParamInfo;
30  import org.kuali.student.r2.core.search.dto.SearchRequestInfo;
31  import org.kuali.student.r2.core.search.dto.SearchResultCellInfo;
32  import org.kuali.student.r2.core.search.dto.SearchResultInfo;
33  import org.kuali.student.r2.core.search.dto.SearchResultRowInfo;
34  import org.kuali.student.r2.core.search.dto.SubSearchInfo;
35  import org.kuali.student.r2.core.search.dto.SubSearchParamMappingInfo;
36  import org.kuali.student.r2.core.search.service.SearchService;
37  
38  import java.util.ArrayList;
39  import java.util.Date;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  import org.kuali.student.r2.common.exceptions.InvalidParameterException;
44  
45  /**
46   * This still needs a few things
47   * 1 - no search meta(sort, pagination) is implemented
48   * 2 - a way to do subselects should be implemented to reduce the processing and sheer size of the unions
49   * (for example if searching for LO and the related CLU by the LO description, we need to match ALL CLUs 
50   * with just the LOs that match, meaning if we had 1000 clus, and 10 LOs we would be comparing 10000 results)
51   * 
52   *
53   */
54  
55  /**
56   * @author Daniel Epstein
57   *
58   */
59  public class CrossSearchManager {
60  	private SearchService searchDispatcher;
61  
62  	public SearchResultInfo doCrossSearch(SearchRequestInfo searchRequest, CrossSearchTypeInfo crossSearchType, ContextInfo contextInfo) throws MissingParameterException, PermissionDeniedException, OperationFailedException, InvalidParameterException {
63  		SearchResultInfo searchResult = new SearchResultInfo();
64  		
65  		Map<String,SearchResultInfo> subSearchResults = new HashMap<String,SearchResultInfo>();
66  		
67  		//First perform all the subsearches
68  		for(SubSearchInfo subSearch:crossSearchType.getSubSearches()){
69  			//Map the parameters to the subsearch
70  			SearchRequestInfo subSearchRequest = new SearchRequestInfo();
71  			
72  			subSearchRequest.setSearchKey(subSearch.getSearchkey());
73  			subSearchRequest.setParams(new ArrayList<SearchParamInfo>());
74              subSearchRequest.setSortColumn(searchRequest.getSortColumn());
75              subSearchRequest.setSortDirection(searchRequest.getSortDirection());
76  			
77  			//For each param mapping, map the paramvalue from the cross search to the sub search
78  			for(SubSearchParamMappingInfo paramMapping:subSearch.getSubSearchParamMappings()){
79  				for(SearchParamInfo crossSearchParam:searchRequest.getParams()){
80  					if(paramMapping.getCrossSearchParam().equals(crossSearchParam.getKey())){
81  						SearchParamInfo subSearchParam = new SearchParamInfo();
82  						subSearchParam.setKey(paramMapping.getSubSearchParam());
83                          subSearchParam.setValues(crossSearchParam.getValues());
84  						subSearchRequest.getParams().add(subSearchParam);
85  					}
86  				}
87  			}
88  			SearchResultInfo subSearchResult = searchDispatcher.search(subSearchRequest, contextInfo);
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,SearchResultInfo> subSearchResult:subSearchResults.entrySet()){
96  				for(SearchResultRowInfo row:subSearchResult.getValue().getRows()){
97  					SearchResultRowInfo 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,SearchResultRowInfo>> allPermutations = unionOfAllRows(subSearchResults);
104 	
105 			for(Map<String,SearchResultRowInfo> permutation:allPermutations){
106 				if(meetsCriteria(permutation,crossSearchType,crossSearchType.getJoinCriteria())){
107 					SearchResultRowInfo 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 SearchResultInfo metaFilter(SearchResultInfo searchResult,
124 		SearchRequestInfo searchRequest) {
125 		
126         searchResult.setTotalResults(searchResult.getRows().size());
127 
128 		searchResult.sortRows();		
129 		
130 		//Paginate if we need to
131 		if(searchRequest.getMaxResults()!=null){
132 			int fromIndex=0;
133 			if(searchRequest.getStartAt()!=null){
134 				fromIndex=searchRequest.getStartAt();
135 			}
136 			int toIndex = fromIndex+searchRequest.getMaxResults();
137 			SearchResultInfo pagedResult = new SearchResultInfo();
138 			for (int i=fromIndex; i <= toIndex; i++) {
139 				if (!(searchResult.getRows().size() < i+1)) {
140 					pagedResult.getRows().add(searchResult.getRows().get(i));
141 				}
142 			}
143             pagedResult.setTotalResults(searchResult.getRows().size());
144 			searchResult = pagedResult;
145 		}
146 		return searchResult;
147 	}
148 
149 	/**
150 	 * Maps results from multiple searches into a single result row
151 	 *
152 	 * @param permutation
153 	 * @param crossSearchType
154 	 * @return a mapped SearchResultRowInfo
155 	 */
156 	private SearchResultRowInfo mapResultRow(
157 			Map<String, SearchResultRowInfo> permutation,
158 			CrossSearchTypeInfo crossSearchType) {
159 		//FIXME this is pretty inefficient to loop through everything... a map structure for the cells might be better
160 		SearchResultRowInfo resultRow = new SearchResultRowInfo();
161 		for(JoinResultMappingInfo resultMapping: crossSearchType.getJoinResultMappings()){
162 			for(SearchResultCellInfo cell: permutation.get(resultMapping.getSubSearchKey()).getCells()){
163 				if(resultMapping.getSubSearchResultParam().equals(cell.getKey())){
164 					SearchResultCellInfo mappedCell = new SearchResultCellInfo();
165 					mappedCell.setKey(resultMapping.getResultParam());
166 					mappedCell.setValue(cell.getValue());
167 					resultRow.getCells().add(mappedCell);
168 					break;//FIXME breaks are bad... but there is no map in the cells
169 				}
170 			}
171 		}
172 		return resultRow;
173 		
174 	}
175 
176 	private SearchResultRowInfo mapResultRow(
177 			String subSearchKey, SearchResultRowInfo row,
178 			CrossSearchTypeInfo crossSearchType) {
179 		SearchResultRowInfo resultRow = new SearchResultRowInfo();
180 		
181 		for(JoinResultMappingInfo resultMapping: crossSearchType.getJoinResultMappings()){
182 			if(subSearchKey.equals(resultMapping.getSubSearchKey())){
183 				for(SearchResultCellInfo cell: row.getCells()){
184 					if(resultMapping.getSubSearchResultParam().equals(cell.getKey())){
185 						SearchResultCellInfo mappedCell = new SearchResultCellInfo();
186 						mappedCell.setKey(resultMapping.getResultParam());
187 						mappedCell.setValue(cell.getValue());
188 						resultRow.getCells().add(mappedCell);
189 						break;//FIXME breaks are bad... but there is no map in the cells
190 					}
191 				}
192 			}
193 		}
194 		return resultRow;
195 	}
196 	/**
197 	 * Checks each comparison of the join criteria and recursively checks through nested criteria.  
198 	 * Short circuits for false 'AND' joins and true 'OR' joins
199 	 * @param permutation
200 	 * @param crossSearchType
201 	 * @param joinCriteria
202 	 * @return whether the criteria is met
203 	 */
204 	private boolean meetsCriteria(Map<String, SearchResultRowInfo> permutation,
205 			CrossSearchTypeInfo crossSearchType, JoinCriteriaInfo joinCriteria) throws OperationFailedException {
206 
207 		JoinType joinType = joinCriteria.getJoinType();
208 		
209 		//Check actual comparisons
210 		for(JoinComparisonInfo comparison:joinCriteria.getComparisons()){
211 			SearchResultRowInfo leftResultRow =  permutation.get(comparison.getLeftHandSide().getSubSearchKey());
212 			String leftResultValue = null;
213 			if(leftResultRow!=null){
214 				for(SearchResultCellInfo cell: leftResultRow.getCells()){
215 					if(comparison.getLeftHandSide().getParam().equals(cell.getKey())){
216 						leftResultValue = cell.getValue();
217 						break;//FIXME breaks are bad... but there is no map in the cells
218 					}
219 				}
220 			}
221 			
222 			SearchResultRowInfo rightResultRow =  permutation.get(comparison.getRightHandSide().getSubSearchKey());
223 			String rightResultValue = null;
224 			if(rightResultRow!=null){
225 				for(SearchResultCellInfo cell: rightResultRow.getCells()){
226 					if(comparison.getRightHandSide().getParam().equals(cell.getKey())){
227 						rightResultValue = cell.getValue();
228 						break;//FIXME breaks are bad... but there is no map in the cells
229 					}
230 				}
231 			}			
232 			
233 			//Get the compare type for the 
234 			//TODO get the types for the params!
235 			if(leftResultValue==null||rightResultValue==null){
236 				int i=0;i++;
237 			}
238 			if(compare(null, leftResultValue,rightResultValue,comparison.getType())){
239 				if(JoinType.OR.equals(joinType)){
240 					return true;
241 				}
242 			}else{
243 				if(JoinType.AND.equals(joinType)){
244 					return false;
245 				}
246 			}
247 		}
248 		
249 		//Check all subcriteria next
250 		for(JoinCriteriaInfo subCriteria: joinCriteria.getJoinCriteria()){
251 			if(meetsCriteria(permutation, crossSearchType, subCriteria)){
252 				if(JoinType.OR.equals(joinType)){
253 					return true;
254 				}
255 			}else{
256 				if(JoinType.AND.equals(joinType)){
257 					return false;
258 				}
259 			}
260 		}
261 		
262 		if(JoinType.AND.equals(joinType)){
263 			return true;
264 		}
265 		if(JoinType.OR.equals(joinType)){
266 			return false;
267 		}
268 		
269 		return false;
270 	}
271 
272 	/**
273 	 * @param searchResults
274 	 * @return a list of all possible combinations of rows
275 	 */
276 	private List <Map<String,SearchResultRowInfo>> unionOfAllRows(Map<String, SearchResultInfo> searchResults){
277 		List <Map<String,SearchResultRowInfo>> r = new ArrayList<Map<String,SearchResultRowInfo>>();
278 		for(Map.Entry<String,SearchResultInfo> x:searchResults.entrySet()){
279 			List<Map<String,SearchResultRowInfo>> t = new ArrayList<Map<String,SearchResultRowInfo>>();
280 			if(x.getValue()!=null&&x.getValue().getRows()!=null){
281 				for(SearchResultRowInfo y:x.getValue().getRows()){
282 					for(Map<String,SearchResultRowInfo> i:r){
283 						Map<String,SearchResultRowInfo> unions =  new HashMap<String,SearchResultRowInfo>();
284 						unions.putAll(i);
285 						unions.put(x.getKey(), y);
286 						t.add(unions);
287 					}
288 					if(r.size()==0){
289 						Map<String,SearchResultRowInfo> unions  =  new HashMap<String,SearchResultRowInfo>();
290 						unions.put(x.getKey(), y);
291 						t.add(unions);
292 					}
293 				}
294 			}
295 			r = t;
296 		}
297 		return r;
298 	}	
299 	
300 	private enum DataType{STRING,INT,BOOLEAN,DATE}
301 	
302 
303 
304 	private boolean compare(DataType dataType, String left, String right,
305 			ComparisonType type ) throws OperationFailedException {
306 		//FIXME Right now DataType is always null, needs to be addressed by fixing JIRA KSCOR-505
307 		try{
308 			Integer leftInteger = Integer.parseInt(left);
309 			Integer rightInteger = Integer.parseInt(right);
310 			return compare(leftInteger,rightInteger,type);
311 		}catch(NumberFormatException e){
312 		}
313 
314 
315         if(left != null && right != null) {
316             if(("true".equals(left.toLowerCase())||"false".equals(left.toLowerCase())) &&
317                     ("true".equals(right.toLowerCase())||"false".equals(right.toLowerCase()))) {
318                 Boolean leftBoolean = Boolean.parseBoolean(left);
319                 Boolean rightBoolean = Boolean.parseBoolean(right);
320                 return compare(leftBoolean, rightBoolean, type);
321             }
322         }
323         try{
324             Date leftDate = null, rightDate = null;
325             if(left != null) {
326                 leftDate = DateFormatters.DEFAULT_DATE_FORMATTER.parse(left);
327             }
328             if(right != null) {
329                 rightDate = DateFormatters.DEFAULT_DATE_FORMATTER.parse(right);
330             }
331             return compare(leftDate, rightDate, type);
332         }catch(IllegalArgumentException e){
333         }
334 		return compare(left, right, type);
335 	}
336 	
337     private boolean compare(Comparable left, Comparable right, ComparisonType type) throws OperationFailedException {
338 
339         if(left == null || right == null) {
340             if(type == ComparisonType.EQUALS) {
341                 return left == right;
342             }
343             else if(type == ComparisonType.NOTEQUALS) {
344                 return left != right;
345             }
346             else {
347                 throw new OperationFailedException("Comparison type " + type.toString() + " undefined for null values");
348             }
349         }
350 
351         switch (type) {
352             case EQUALS:
353                 return left.equals(right);
354             case GREATERTHAN:
355                 return left.compareTo(right) > 0;
356             case GREATERTHANEQUALS:
357                 return left.compareTo(right) >= 0;
358             case LESSTHAN:
359                 return left.compareTo(right) < 0;
360             case LESSTHANEQUALS:
361                 return left.compareTo(right) <= 0;
362             case NOTEQUALS:
363                 return !left.equals(right);
364             default:
365                 throw new OperationFailedException("Unsupported ComparisonType: " + type);
366         }
367     }
368 
369 	public void setSearchDispatcher(SearchService searchDispatcher) {
370 		this.searchDispatcher = searchDispatcher;
371 	}
372 
373 	public SearchService getSearchDispatcher() {
374 		return searchDispatcher;
375 	}
376 		
377 }