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}