001/** 002 * Copyright 2010 The Kuali Foundation Licensed under the 003 * Educational Community License, Version 2.0 (the "License"); you may 004 * not use this file except in compliance with the License. You may 005 * obtain a copy of the License at 006 * 007 * http://www.osedu.org/licenses/ECL-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, 010 * software distributed under the License is distributed on an "AS IS" 011 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 012 * or implied. See the License for the specific language governing 013 * permissions and limitations under the License. 014 */ 015 016package org.kuali.student.r2.common.class1.search; 017 018import org.kuali.student.r2.common.dto.ContextInfo; 019import org.kuali.student.r2.common.exceptions.MissingParameterException; 020import org.kuali.student.r2.common.exceptions.OperationFailedException; 021import org.kuali.student.r2.common.exceptions.PermissionDeniedException; 022import org.kuali.student.r2.common.util.date.DateFormatters; 023import org.kuali.student.r2.core.search.dto.CrossSearchTypeInfo; 024import org.kuali.student.r2.core.search.dto.JoinComparisonInfo; 025import org.kuali.student.r2.core.search.dto.JoinComparisonInfo.ComparisonType; 026import org.kuali.student.r2.core.search.dto.JoinCriteriaInfo; 027import org.kuali.student.r2.core.search.dto.JoinCriteriaInfo.JoinType; 028import org.kuali.student.r2.core.search.dto.JoinResultMappingInfo; 029import org.kuali.student.r2.core.search.dto.SearchParamInfo; 030import org.kuali.student.r2.core.search.dto.SearchRequestInfo; 031import org.kuali.student.r2.core.search.dto.SearchResultCellInfo; 032import org.kuali.student.r2.core.search.dto.SearchResultInfo; 033import org.kuali.student.r2.core.search.dto.SearchResultRowInfo; 034import org.kuali.student.r2.core.search.dto.SubSearchInfo; 035import org.kuali.student.r2.core.search.dto.SubSearchParamMappingInfo; 036import org.kuali.student.r2.core.search.service.SearchService; 037 038import java.util.ArrayList; 039import java.util.Date; 040import java.util.HashMap; 041import java.util.List; 042import java.util.Map; 043import org.kuali.student.r2.common.exceptions.InvalidParameterException; 044 045/** 046 * This still needs a few things 047 * 1 - no search meta(sort, pagination) is implemented 048 * 2 - a way to do subselects should be implemented to reduce the processing and sheer size of the unions 049 * (for example if searching for LO and the related CLU by the LO description, we need to match ALL CLUs 050 * with just the LOs that match, meaning if we had 1000 clus, and 10 LOs we would be comparing 10000 results) 051 * 052 * 053 */ 054 055/** 056 * @author Daniel Epstein 057 * 058 */ 059public class CrossSearchManager { 060 private SearchService searchDispatcher; 061 062 public SearchResultInfo doCrossSearch(SearchRequestInfo searchRequest, CrossSearchTypeInfo crossSearchType, ContextInfo contextInfo) throws MissingParameterException, PermissionDeniedException, OperationFailedException, InvalidParameterException { 063 SearchResultInfo searchResult = new SearchResultInfo(); 064 065 Map<String,SearchResultInfo> subSearchResults = new HashMap<String,SearchResultInfo>(); 066 067 //First perform all the subsearches 068 for(SubSearchInfo subSearch:crossSearchType.getSubSearches()){ 069 //Map the parameters to the subsearch 070 SearchRequestInfo subSearchRequest = new SearchRequestInfo(); 071 072 subSearchRequest.setSearchKey(subSearch.getSearchkey()); 073 subSearchRequest.setParams(new ArrayList<SearchParamInfo>()); 074 subSearchRequest.setSortColumn(searchRequest.getSortColumn()); 075 subSearchRequest.setSortDirection(searchRequest.getSortDirection()); 076 077 //For each param mapping, map the paramvalue from the cross search to the sub search 078 for(SubSearchParamMappingInfo paramMapping:subSearch.getSubSearchParamMappings()){ 079 for(SearchParamInfo crossSearchParam:searchRequest.getParams()){ 080 if(paramMapping.getCrossSearchParam().equals(crossSearchParam.getKey())){ 081 SearchParamInfo subSearchParam = new SearchParamInfo(); 082 subSearchParam.setKey(paramMapping.getSubSearchParam()); 083 subSearchParam.setValues(crossSearchParam.getValues()); 084 subSearchRequest.getParams().add(subSearchParam); 085 } 086 } 087 } 088 SearchResultInfo subSearchResult = searchDispatcher.search(subSearchRequest, contextInfo); 089 subSearchResults.put(subSearch.getKey(), subSearchResult); 090 } 091 092 //merge the subsearches together using the join rules 093 if(crossSearchType.getJoinCriteria().getComparisons().isEmpty()){ 094 //If the root join has no criteria then do a simple union of rows 095 for(Map.Entry<String,SearchResultInfo> subSearchResult:subSearchResults.entrySet()){ 096 for(SearchResultRowInfo row:subSearchResult.getValue().getRows()){ 097 SearchResultRowInfo mappedResult = mapResultRow(subSearchResult.getKey(),row,crossSearchType); 098 searchResult.getRows().add(mappedResult); 099 } 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}