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#getNumPages()
407     */
408    @Override
409    public int getNumPages() {
410
411        return m_solrResultList == null
412        ? 1
413        : m_controller.getPagination().getConfig().getNumPages(m_solrResultList.getNumFound());
414    }
415
416    /**
417     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getPageNavFirst()
418     */
419    @Override
420    public int getPageNavFirst() {
421
422        final int page = m_controller.getPagination().getState().getCurrentPage()
423            - ((m_controller.getPagination().getConfig().getPageNavLength() - 1) / 2);
424        return page < 1 ? 1 : page;
425    }
426
427    /**
428     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getPageNavLast()
429     */
430    @Override
431    public int getPageNavLast() {
432
433        final int page = m_controller.getPagination().getState().getCurrentPage()
434            + ((m_controller.getPagination().getConfig().getPageNavLength()) / 2);
435        return page > getNumPages() ? getNumPages() : page;
436    }
437
438    /**
439     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getRangeFacet()
440     */
441    @SuppressWarnings("rawtypes")
442    public Map<String, RangeFacet> getRangeFacet() {
443
444        return m_rangeFacetMap;
445    }
446
447    /**
448     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getRangeFacets()
449     */
450    @SuppressWarnings("rawtypes")
451    public Collection<RangeFacet> getRangeFacets() {
452
453        return m_rangeFacetMap.values();
454    }
455
456    /**
457     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getSearchResults()
458     */
459    @Override
460    public Collection<I_CmsSearchResourceBean> getSearchResults() {
461
462        return m_foundResources;
463    }
464
465    /**
466     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getStart()
467     */
468    @Override
469    public Long getStart() {
470
471        return m_start;
472    }
473
474    /**
475     * @see org.opencms.jsp.search.result.I_CmsSearchResultWrapper#getStateParameters()
476     */
477    public CmsSearchStateParameters getStateParameters() {
478
479        Map<String, String[]> parameters = new HashMap<String, String[]>();
480        m_controller.addParametersForCurrentState(parameters);
481        return new CmsSearchStateParameters(this, parameters);
482    }
483
484    /** Converts the search results from CmsSearchResource to CmsSearchResourceBean.
485     * @param searchResults The collection of search results to transform.
486     */
487    protected void convertSearchResults(final Collection<CmsSearchResource> searchResults) {
488
489        m_foundResources = new ArrayList<I_CmsSearchResourceBean>();
490        for (final CmsSearchResource searchResult : searchResults) {
491            m_foundResources.add(new CmsSearchResourceBean(searchResult, m_cmsObject));
492        }
493    }
494
495    /** Removes the !{ex=...} prefix from the query.
496     * @param q the original query
497     * @return the query with the prefix !{ex=...} removed.
498     */
499    private String removeLocalParamPrefix(final String q) {
500
501        int index = q.indexOf('}');
502        if (index >= 0) {
503            return q.substring(index + 1);
504        }
505        return q;
506    }
507
508}