001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.search.result;
029
030import org.opencms.file.CmsObject;
031import org.opencms.jsp.search.controller.I_CmsSearchControllerDidYouMean;
032import org.opencms.jsp.search.controller.I_CmsSearchControllerFacetField;
033import org.opencms.jsp.search.controller.I_CmsSearchControllerFacetQuery;
034import org.opencms.jsp.search.controller.I_CmsSearchControllerFacetRange;
035import org.opencms.jsp.search.controller.I_CmsSearchControllerMain;
036import org.opencms.search.CmsSearchException;
037import org.opencms.search.CmsSearchResource;
038import org.opencms.search.solr.CmsSolrQuery;
039import org.opencms.search.solr.CmsSolrResultList;
040import org.opencms.util.CmsCollectionsGenericWrapper;
041
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047
048import org.apache.commons.collections.Transformer;
049import org.apache.solr.client.solrj.response.FacetField;
050import org.apache.solr.client.solrj.response.RangeFacet;
051import org.apache.solr.client.solrj.response.SpellCheckResponse.Suggestion;
052
053/** Wrapper for the whole search result. Also allowing to access the search form controller. */
054public class CmsSearchResultWrapper implements I_CmsSearchResultWrapper {
055
056    /** The result list as returned normally. */
057    final CmsSolrResultList m_solrResultList;
058    /** The collection of found resources/documents, already wrapped as {@code I_CmsSearchResourceBean}. */
059    private Collection<I_CmsSearchResourceBean> m_foundResources;
060    /** The first index of the documents displayed. */
061    private final Long m_start;
062    /** The last index of the documents displayed. */
063    private final int m_end;
064    /** The number of found results. */
065    private final long m_numFound;
066    /** The maximal score of the results. */
067    private final Float m_maxScore;
068    /** The main controller for the search form. */
069    final I_CmsSearchControllerMain m_controller;
070    /** Map from field facet names to the facets as given by the search result. */
071    private Map<String, FacetField> m_fieldFacetMap;
072    /** Map from range facet names to the facets as given by the search result. */
073    @SuppressWarnings("rawtypes")
074    private Map<String, RangeFacet> m_rangeFacetMap;
075    /** Map from facet names to the facet entries checked, but not part of the result. */
076    private Map<String, List<String>> m_missingFieldFacetEntryMap;
077    /** Map from facet names to the facet entries checked, but not part of the result. */
078    private Map<String, List<String>> m_missingRangeFacetEntryMap;
079    /** Query facet items that are checked, but not part of the result. */
080    private List<String> m_missingQueryFacetEntries;
081    /** Map with the facet items of the query facet and their counts. */
082    private Map<String, Integer> m_facetQuery;
083    /** CmsObject. */
084    private final CmsObject m_cmsObject;
085    /** Search exception, if one occurs. */
086    private final CmsSearchException m_exception;
087    /** The search query sent to Solr. */
088    private final CmsSolrQuery m_query;
089
090    /** Constructor taking the main search form controller and the result list as normally returned.
091     * @param controller The main search form controller.
092     * @param resultList The result list as returned from OpenCms' embedded Solr server.
093     * @param query The complete query send to Solr.
094     * @param cms The Cms object used to access XML contents, if wanted.
095     * @param exception Search exception, or <code>null</code> if no exception occurs.
096     */
097    @SuppressWarnings("rawtypes")
098    public CmsSearchResultWrapper(
099        final I_CmsSearchControllerMain controller,
100        final CmsSolrResultList resultList,
101        final CmsSolrQuery query,
102        final CmsObject cms,
103        final CmsSearchException exception) {
104
105        m_controller = controller;
106        m_solrResultList = resultList;
107        m_cmsObject = cms;
108        m_exception = exception;
109        m_query = query;
110        if (resultList != null) {
111            convertSearchResults(resultList);
112            final long l = resultList.getStart() == null ? 1 : resultList.getStart().longValue() + 1;
113            m_start = Long.valueOf(l);
114            m_end = resultList.getEnd();
115            m_numFound = resultList.getNumFound();
116            m_maxScore = resultList.getMaxScore();
117            if (resultList.getFacetQuery() != null) {
118                Map<String, Integer> originalMap = resultList.getFacetQuery();
119                m_facetQuery = new HashMap<String, Integer>(originalMap.size());
120                for (String q : resultList.getFacetQuery().keySet()) {
121                    m_facetQuery.put(removeLocalParamPrefix(q), originalMap.get(q));
122                }
123            }
124            List<RangeFacet> rangeFacets = resultList.getFacetRanges();
125            if (null != rangeFacets) {
126                m_rangeFacetMap = new HashMap<String, RangeFacet>(rangeFacets.size());
127                for (RangeFacet facet : rangeFacets) {
128                    m_rangeFacetMap.put(facet.getName(), facet);
129                }
130            }
131        } else {
132            m_start = null;
133            m_end = 0;
134            m_numFound = 0;
135            m_maxScore = null;
136        }
137        if (null == m_rangeFacetMap) {
138            m_rangeFacetMap = new HashMap<String, RangeFacet>();
139        }
140    }
141
142    /**
143     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getController()
144     */
145    @Override
146    public I_CmsSearchControllerMain getController() {
147
148        return m_controller;
149    }
150
151    /**
152     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getDidYouMeanCollated()
153     */
154    public String getDidYouMeanCollated() {
155
156        String suggestion = null;
157        I_CmsSearchControllerDidYouMean didYouMeanController = getController().getDidYouMean();
158        if ((null != didYouMeanController) && didYouMeanController.getConfig().getCollate()) {
159            if ((m_solrResultList != null) && (m_solrResultList.getSpellCheckResponse() != null)) {
160                suggestion = m_solrResultList.getSpellCheckResponse().getCollatedResult();
161            }
162        }
163        return suggestion;
164    }
165
166    /**
167     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getDidYouMeanSuggestion()
168     */
169    public Suggestion getDidYouMeanSuggestion() {
170
171        I_CmsSearchControllerDidYouMean didYouMeanController = getController().getDidYouMean();
172        Suggestion usedSuggestion = null;
173        if ((null != didYouMeanController)
174            && ((m_solrResultList != null) && (m_solrResultList.getSpellCheckResponse() != null))) {
175            // find most suitable suggestion
176            List<Suggestion> suggestionList = m_solrResultList.getSpellCheckResponse().getSuggestions();
177            int queryLength = m_controller.getDidYouMean().getState().getQuery().length();
178            int minDistance = queryLength + 1;
179            for (Suggestion suggestion : suggestionList) {
180                int currentDistance = Math.abs(queryLength - suggestion.getToken().length());
181                if (currentDistance < minDistance) {
182                    usedSuggestion = suggestion;
183                    minDistance = currentDistance;
184                }
185            }
186        }
187        return usedSuggestion;
188    }
189
190    /**
191     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getEmptyStateParameters()
192     */
193    public I_CmsSearchStateParameters getEmptyStateParameters() {
194
195        Map<String, String[]> parameters = new HashMap<String, String[]>();
196        return new CmsSearchStateParameters(this, parameters);
197    }
198
199    /**
200     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getEnd()
201     */
202    @Override
203    public int getEnd() {
204
205        return m_end;
206    }
207
208    /**
209     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getException()
210     */
211    public CmsSearchException getException() {
212
213        return m_exception;
214    }
215
216    /**
217     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getFacetQuery()
218     */
219    @Override
220    public Map<String, Integer> getFacetQuery() {
221
222        return m_facetQuery;
223    }
224
225    /**
226     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getFieldFacet()
227     */
228    @Override
229    public Map<String, FacetField> getFieldFacet() {
230
231        if (m_fieldFacetMap == null) {
232            m_fieldFacetMap = CmsCollectionsGenericWrapper.createLazyMap(new Transformer() {
233
234                @Override
235                public Object transform(final Object fieldName) {
236
237                    return m_solrResultList == null ? null : m_solrResultList.getFacetField(fieldName.toString());
238                }
239            });
240        }
241        return m_fieldFacetMap;
242    }
243
244    /**
245     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getFieldFacets()
246     */
247    @Override
248    public Collection<FacetField> getFieldFacets() {
249
250        return m_solrResultList == null ? null : m_solrResultList.getFacetFields();
251    }
252
253    /**
254     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getFinalQuery()
255     */
256    public CmsSolrQuery getFinalQuery() {
257
258        return m_query;
259    }
260
261    /**
262     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getHighlighting()
263     */
264    @Override
265    public Map<String, Map<String, List<String>>> getHighlighting() {
266
267        return m_solrResultList == null ? null : m_solrResultList.getHighLighting();
268    }
269
270    /**
271     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getMaxScore()
272     */
273    @Override
274    public Float getMaxScore() {
275
276        return m_maxScore;
277    }
278
279    /**
280     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getMissingSelectedFieldFacetEntries()
281     */
282    @Override
283    public Map<String, List<String>> getMissingSelectedFieldFacetEntries() {
284
285        if (m_missingFieldFacetEntryMap == null) {
286            m_missingFieldFacetEntryMap = CmsCollectionsGenericWrapper.createLazyMap(new Transformer() {
287
288                @Override
289                public Object transform(final Object fieldName) {
290
291                    FacetField facetResult = m_solrResultList == null
292                    ? null
293                    : m_solrResultList.getFacetField(fieldName.toString());
294                    I_CmsSearchControllerFacetField facetController = m_controller.getFieldFacets().getFieldFacetController().get(
295                        fieldName.toString());
296                    List<String> result = new ArrayList<String>();
297
298                    if (null != facetController) {
299
300                        List<String> checkedEntries = facetController.getState().getCheckedEntries();
301                        if (null != facetResult) {
302                            List<String> returnedValues = new ArrayList<String>(facetResult.getValues().size());
303                            for (FacetField.Count value : facetResult.getValues()) {
304                                returnedValues.add(value.getName());
305                            }
306                            for (String checked : checkedEntries) {
307                                if (!returnedValues.contains(checked)) {
308                                    result.add(checked);
309                                }
310                            }
311                        } else {
312                            result = checkedEntries;
313                        }
314                    }
315                    return result;
316                }
317            });
318        }
319        return m_missingFieldFacetEntryMap;
320    }
321
322    /**
323     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getMissingSelectedQueryFacetEntries()
324     */
325    public List<String> getMissingSelectedQueryFacetEntries() {
326
327        if (null == m_missingQueryFacetEntries) {
328
329            Collection<String> returnedValues = getFacetQuery().keySet();
330
331            I_CmsSearchControllerFacetQuery facetController = m_controller.getQueryFacet();
332
333            m_missingQueryFacetEntries = new ArrayList<String>();
334
335            if (null != facetController) {
336
337                List<String> checkedEntries = facetController.getState().getCheckedEntries();
338                if (null != returnedValues) {
339                    for (String checked : checkedEntries) {
340                        if (!returnedValues.contains(checked)) {
341                            m_missingQueryFacetEntries.add(checked);
342                        }
343                    }
344                } else {
345                    m_missingQueryFacetEntries = checkedEntries;
346                }
347            }
348        }
349        return m_missingQueryFacetEntries;
350    }
351
352    /**
353     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getMissingSelectedRangeFacetEntries()
354     */
355    public Map<String, List<String>> getMissingSelectedRangeFacetEntries() {
356
357        if (m_missingRangeFacetEntryMap == null) {
358            m_missingRangeFacetEntryMap = CmsCollectionsGenericWrapper.createLazyMap(new Transformer() {
359
360                @Override
361                public Object transform(final Object fieldName) {
362
363                    @SuppressWarnings("rawtypes")
364                    RangeFacet facetResult = m_rangeFacetMap.get(fieldName);
365                    I_CmsSearchControllerFacetRange facetController = m_controller.getRangeFacets().getRangeFacetController().get(
366                        fieldName.toString());
367                    List<String> result = new ArrayList<String>();
368
369                    if (null != facetController) {
370
371                        List<String> checkedEntries = facetController.getState().getCheckedEntries();
372                        if (null != facetResult) {
373                            List<String> returnedValues = new ArrayList<String>(facetResult.getCounts().size());
374                            for (Object value : facetResult.getCounts()) {
375                                //TODO: Should yield RangeFacet.Count - but somehow does not!?!?
376                                // Hence, the cast should not be necessary at all.
377                                returnedValues.add(((RangeFacet.Count)value).getValue());
378                            }
379                            for (String checked : checkedEntries) {
380                                if (!returnedValues.contains(checked)) {
381                                    result.add(checked);
382                                }
383                            }
384                        } else {
385                            result = checkedEntries;
386                        }
387                    }
388                    return result;
389                }
390            });
391        }
392        return m_missingRangeFacetEntryMap;
393
394    }
395
396    /**
397     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getNumFound()
398     */
399    @Override
400    public long getNumFound() {
401
402        return m_numFound;
403    }
404
405    /**
406     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getNumMaxReturned()
407     */
408    public long getNumMaxReturned() {
409
410        long maxReturnedResults = Integer.valueOf(
411            m_controller.getCommon().getConfig().getMaxReturnedResults()).longValue();
412        return (maxReturnedResults < 0) || (maxReturnedResults > getNumFound()) ? getNumFound() : maxReturnedResults;
413    }
414
415    /**
416     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getNumPages()
417     */
418    @Override
419    public int getNumPages() {
420
421        return m_solrResultList == null ? 1 : m_controller.getPagination().getConfig().getNumPages(getNumMaxReturned());
422    }
423
424    /**
425     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getPageNavFirst()
426     */
427    @Override
428    public int getPageNavFirst() {
429
430        final int page = m_controller.getPagination().getState().getCurrentPage()
431            - ((m_controller.getPagination().getConfig().getPageNavLength() - 1) / 2);
432        return page < 1 ? 1 : page;
433    }
434
435    /**
436     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getPageNavLast()
437     */
438    @Override
439    public int getPageNavLast() {
440
441        final int page = m_controller.getPagination().getState().getCurrentPage()
442            + ((m_controller.getPagination().getConfig().getPageNavLength()) / 2);
443        return page > getNumPages() ? getNumPages() : page;
444    }
445
446    /**
447     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getRangeFacet()
448     */
449    @SuppressWarnings("rawtypes")
450    public Map<String, RangeFacet> getRangeFacet() {
451
452        return m_rangeFacetMap;
453    }
454
455    /**
456     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getRangeFacets()
457     */
458    @SuppressWarnings("rawtypes")
459    public Collection<RangeFacet> getRangeFacets() {
460
461        return m_rangeFacetMap.values();
462    }
463
464    /**
465     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getSearchResults()
466     */
467    @Override
468    public Collection<I_CmsSearchResourceBean> getSearchResults() {
469
470        return m_foundResources;
471    }
472
473    /**
474     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getStart()
475     */
476    @Override
477    public Long getStart() {
478
479        return m_start;
480    }
481
482    /**
483     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getStateParameters()
484     */
485    public CmsSearchStateParameters getStateParameters() {
486
487        Map<String, String[]> parameters = new HashMap<String, String[]>();
488        m_controller.addParametersForCurrentState(parameters);
489        return new CmsSearchStateParameters(this, parameters);
490    }
491
492    /** Converts the search results from CmsSearchResource to CmsSearchResourceBean.
493     * @param searchResults The collection of search results to transform.
494     */
495    protected void convertSearchResults(final Collection<CmsSearchResource> searchResults) {
496
497        m_foundResources = new ArrayList<I_CmsSearchResourceBean>();
498        for (final CmsSearchResource searchResult : searchResults) {
499            m_foundResources.add(new CmsSearchResourceBean(searchResult, m_cmsObject));
500        }
501    }
502
503    /** Removes the !{ex=...} prefix from the query.
504     * @param q the original query
505     * @return the query with the prefix !{ex=...} removed.
506     */
507    private String removeLocalParamPrefix(final String q) {
508
509        int index = q.indexOf('}');
510        if (index >= 0) {
511            return q.substring(index + 1);
512        }
513        return q;
514    }
515
516}