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.ui.components;
029
030import org.opencms.file.CmsObject;
031import org.opencms.jsp.search.config.parser.CmsSimpleSearchConfigurationParser;
032import org.opencms.main.CmsLog;
033import org.opencms.relations.CmsCategory;
034import org.opencms.relations.CmsCategoryService;
035import org.opencms.search.solr.CmsSolrResultList;
036import org.opencms.ui.A_CmsUI;
037import org.opencms.ui.CmsVaadinUtils;
038import org.opencms.ui.apps.Messages;
039import org.opencms.ui.apps.lists.CmsListManager;
040import org.opencms.util.CmsStringUtil;
041
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.Collections;
045import java.util.HashMap;
046import java.util.List;
047import java.util.Locale;
048import java.util.Map;
049
050import org.apache.commons.logging.Log;
051import org.apache.solr.client.solrj.response.FacetField;
052import org.apache.solr.client.solrj.response.FacetField.Count;
053import org.apache.solr.client.solrj.response.RangeFacet;
054
055import org.joda.time.DateTime;
056import org.joda.time.DateTimeZone;
057import org.joda.time.format.DateTimeFormatter;
058import org.joda.time.format.ISODateTimeFormat;
059
060import com.vaadin.ui.Button;
061import com.vaadin.ui.Button.ClickEvent;
062import com.vaadin.ui.Button.ClickListener;
063import com.vaadin.ui.Component;
064import com.vaadin.ui.GridLayout;
065import com.vaadin.ui.Label;
066import com.vaadin.ui.Panel;
067import com.vaadin.ui.UI;
068import com.vaadin.ui.VerticalLayout;
069import com.vaadin.ui.themes.ValoTheme;
070
071/**
072 * Displays search result facets.<p>
073 */
074public class CmsResultFacets extends VerticalLayout {
075
076    /** The logger for this class. */
077    private static final Log LOG = CmsLog.getLog(CmsResultFacets.class.getName());
078
079    /** Style name indicating a facet is selected. */
080    private static final String SELECTED_STYLE = ValoTheme.LABEL_BOLD;
081
082    /** The serial version id. */
083    private static final long serialVersionUID = 7190928063356086124L;
084
085    /** The facet search manager instance. */
086    private I_CmsResultFacetsManager m_manager;
087
088    /** The selected field facets. */
089    private Map<String, List<String>> m_selectedFieldFacets;
090
091    /** The selected folders. */
092    private List<String> m_selectedFolders;
093
094    /** The selected range facets. */
095    private Map<String, List<String>> m_selectedRangeFacets;
096
097    /** The use full category paths flag. */
098    private boolean m_useFullPathCategories;
099
100    /**
101     * Constructor.<p>
102     *
103     * @param manager the facet search manager instance
104     */
105    public CmsResultFacets(I_CmsResultFacetsManager manager) {
106
107        m_manager = manager;
108        m_selectedFieldFacets = new HashMap<String, List<String>>();
109        m_selectedRangeFacets = new HashMap<String, List<String>>();
110        m_selectedFolders = new ArrayList<String>();
111        m_useFullPathCategories = true;
112        addStyleName("v-scrollable");
113        setMargin(true);
114        setSpacing(true);
115    }
116
117    /**
118     * Displays the result facets.<p>
119     * @param solrResultList the search result
120     * @param checkedCategoryFacets checked category facets map
121     * @param checkedFolderFacets checked folder facets map
122     * @param checkedDateFacets checked date facets map
123     * @param cms the CMS context
124     */
125    public void displayFacetResult(
126        CmsSolrResultList solrResultList,
127        Map<String, Boolean> checkedCategoryFacets,
128        Map<String, Boolean> checkedDateFacets,
129        Map<String, Boolean> checkedFolderFacets,
130        CmsObject cms) {
131
132        removeAllComponents();
133        Component categories = prepareCategoryFacets(solrResultList, checkedCategoryFacets, cms);
134        if (categories != null) {
135            addComponent(categories);
136        }
137        Component folders = prepareFolderFacets(solrResultList, checkedFolderFacets, cms);
138        if (folders != null) {
139            addComponent(folders);
140        }
141        Component dates = prepareDateFacets(solrResultList, checkedDateFacets);
142        if (dates != null) {
143            addComponent(dates);
144        }
145    }
146
147    /**
148     * Returns the selected field facets.<p>
149     *
150     * @return the selected field facets
151     */
152    public Map<String, List<String>> getSelectedFieldFacets() {
153
154        return m_selectedFieldFacets;
155    }
156
157    /**
158     * Returns the selected range facets.<p>
159     *
160     * @return the selected range facets
161     */
162    public Map<String, List<String>> getSelectedRangeFactes() {
163
164        return m_selectedRangeFacets;
165    }
166
167    /**
168     * Resets the selected facets.<p>
169     */
170    public void resetFacets() {
171
172        m_selectedFieldFacets.clear();
173        m_selectedRangeFacets.clear();
174    }
175
176    /**
177     * Selects the given field facet.<p>
178     *
179     * @param field the field name
180     * @param value the value
181     */
182    public void selectFieldFacet(String field, String value) {
183
184        m_selectedFieldFacets.clear();
185        m_selectedRangeFacets.clear();
186        m_selectedFieldFacets.put(field, Collections.singletonList(value));
187        m_manager.search(m_selectedFieldFacets, m_selectedRangeFacets);
188    }
189
190    /**
191     * Selects the given range facet.<p>
192     *
193     * @param field the field name
194     * @param value the value
195     */
196    public void selectRangeFacet(String field, String value) {
197
198        m_selectedFieldFacets.clear();
199        m_selectedRangeFacets.clear();
200        m_selectedRangeFacets.put(field, Collections.singletonList(value));
201        m_manager.search(m_selectedFieldFacets, m_selectedRangeFacets);
202    }
203
204    /**
205     * Returns whether the given component is selected.<p>
206     *
207     * @param component the component
208     *
209     * @return <code>true</code> in case selected
210     */
211    boolean isSelected(Component component) {
212
213        return component.getStyleName().contains(SELECTED_STYLE);
214    }
215
216    /**
217     * Resets the selected facets and triggers a new search.<p>
218     */
219    void resetFacetsAndSearch() {
220
221        resetFacets();
222        m_manager.search(m_selectedFieldFacets, m_selectedRangeFacets);
223    }
224
225    /**
226     * Filters the available folder facets.<p>
227     *
228     * @param folderFacets the folder facets
229     * @param cms the CMS context
230     *
231     * @return the filtered facets
232     */
233    private Collection<Count> filterFolderFacets(Collection<Count> folderFacets, CmsObject cms) {
234
235        String siteRoot = cms.getRequestContext().getSiteRoot();
236        if (!siteRoot.endsWith("/")) {
237            siteRoot += "/";
238        }
239        Collection<Count> result = new ArrayList<Count>();
240        for (Count value : folderFacets) {
241            if (value.getName().startsWith(siteRoot) && (value.getName().length() > siteRoot.length())) {
242                if (m_selectedFolders.isEmpty()) {
243                    result.add(value);
244                } else {
245                    for (String folder : m_selectedFolders) {
246                        if (value.getName().startsWith(folder)) {
247                            result.add(value);
248                            break;
249                        }
250                    }
251                }
252            }
253        }
254        return result;
255    }
256
257    /**
258     * Returns the label for the given category.<p>
259     *
260     * @param categoryPath the category
261     * @param cms the CMS context
262     *
263     * @return the label
264     */
265    private String getCategoryLabel(String categoryPath, CmsObject cms) {
266
267        String result = "";
268        if (CmsStringUtil.isEmptyOrWhitespaceOnly(categoryPath)) {
269            return result;
270        }
271        Locale locale = UI.getCurrent().getLocale();
272        CmsCategoryService catService = CmsCategoryService.getInstance();
273        try {
274            if (m_useFullPathCategories) {
275                //cut last slash
276                categoryPath = categoryPath.substring(0, categoryPath.length() - 1);
277                String currentPath = "";
278                boolean isFirst = true;
279                for (String part : categoryPath.split("/")) {
280                    currentPath += part + "/";
281                    CmsCategory cat = catService.localizeCategory(
282                        cms,
283                        catService.readCategory(cms, currentPath, "/"),
284                        locale);
285                    if (!isFirst) {
286                        result += "  /  ";
287                    } else {
288                        isFirst = false;
289                    }
290                    result += cat.getTitle();
291                }
292
293            } else {
294                CmsCategory cat = catService.localizeCategory(
295                    cms,
296                    catService.readCategory(cms, categoryPath, "/"),
297                    locale);
298                result = cat.getTitle();
299            }
300        } catch (Exception e) {
301            LOG.error("Error reading category " + categoryPath + ".", e);
302        }
303        return CmsStringUtil.isEmptyOrWhitespaceOnly(result) ? categoryPath : result;
304    }
305
306    /**
307     * Returns the label for the given folder.<p>
308     *
309     * @param path The folder path
310     *
311     * @return the label
312     */
313    private String getFolderLabel(String path) {
314
315        CmsObject cms = A_CmsUI.getCmsObject();
316        return cms.getRequestContext().removeSiteRoot(path);
317    }
318
319    /**
320     * Prepares the category facets for the given search result.<p>
321     *
322     * @param solrResultList the search result list
323     * @param checkedCategoryFacets checked category facets map
324     * @param cms the CMS context
325     *
326     * @return the category facets component
327     */
328    private Component prepareCategoryFacets(
329        CmsSolrResultList solrResultList,
330        Map<String, Boolean> checkedCategoryFacets,
331        CmsObject cms) {
332
333        FacetField categoryFacets = solrResultList.getFacetField(CmsSimpleSearchConfigurationParser.FIELD_CATEGORIES);
334        if ((categoryFacets != null) && (categoryFacets.getValueCount() > 0)) {
335            VerticalLayout catLayout = new VerticalLayout();
336            for (final Count value : categoryFacets.getValues()) {
337                Button cat = new Button(getCategoryLabel(value.getName(), cms) + " (" + value.getCount() + ")");
338                cat.addStyleName(ValoTheme.BUTTON_TINY);
339                cat.addStyleName(ValoTheme.BUTTON_BORDERLESS);
340                Boolean selected = checkedCategoryFacets.get(value.getName());
341                if ((selected != null) && selected.booleanValue()) {
342                    cat.addStyleName(SELECTED_STYLE);
343                }
344                cat.addClickListener(new ClickListener() {
345
346                    private static final long serialVersionUID = 1L;
347
348                    public void buttonClick(ClickEvent event) {
349
350                        if (isSelected(event.getComponent())) {
351                            resetFacetsAndSearch();
352                        } else {
353                            selectFieldFacet(CmsSimpleSearchConfigurationParser.FIELD_CATEGORIES, value.getName());
354                        }
355                    }
356                });
357                catLayout.addComponent(cat);
358            }
359            Panel catPanel = new Panel(CmsVaadinUtils.getMessageText(Messages.GUI_LISTMANAGER_FACET_CATEGORIES_0));
360            catPanel.setContent(catLayout);
361            return catPanel;
362        } else {
363            return null;
364        }
365    }
366
367    /**
368     * Prepares the date facets for the given search result.<p>
369     *
370     * @param solrResultList the search result list
371     * @param checkedDateFacets checked date facets map
372     *
373     * @return the date facets component
374     */
375    private Component prepareDateFacets(CmsSolrResultList solrResultList, Map<String, Boolean> checkedDateFacets) {
376
377        RangeFacet<?, ?> dateFacets = null;
378        for (RangeFacet<?, ?> rangeFacet : solrResultList.getFacetRanges()) {
379            if (rangeFacet.getName().equals(CmsSimpleSearchConfigurationParser.FIELD_DATE_FACET_NAME)) {
380                dateFacets = rangeFacet;
381            }
382        }
383        DateTimeFormatter isoFormat = ISODateTimeFormat.dateTimeNoMillis();
384        if ((dateFacets != null) && (dateFacets.getCounts().size() > 0)) {
385            GridLayout dateLayout = new GridLayout();
386            dateLayout.setWidth("100%");
387            dateLayout.setColumns(6);
388            String currentYear = null;
389            int row = -2;
390            for (final RangeFacet.Count value : dateFacets.getCounts()) {
391                // parsed date in UTC
392                DateTime parsedDateTime = isoFormat.parseDateTime(value.getValue());
393                // parsed date in server timezone
394                DateTime serverDateTime = parsedDateTime.withZone(DateTimeZone.getDefault());
395                String serverYear = "" + serverDateTime.getYear();
396                if (!serverYear.equals(currentYear)) {
397                    row += 2;
398                    dateLayout.setRows(row + 2);
399                    currentYear = serverYear;
400                    Label year = new Label(currentYear);
401                    year.addStyleName(OpenCmsTheme.PADDING_HORIZONTAL);
402                    dateLayout.addComponent(year, 0, row, 5, row);
403                    row++;
404                }
405                int month = serverDateTime.getMonthOfYear() - 1;
406
407                Button date = new Button(CmsListManager.MONTHS[month] + " (" + value.getCount() + ")");
408                date.addStyleName(ValoTheme.BUTTON_TINY);
409                date.addStyleName(ValoTheme.BUTTON_BORDERLESS);
410                Boolean selected = checkedDateFacets.get(value.getValue());
411                if ((selected != null) && selected.booleanValue()) {
412                    date.addStyleName(SELECTED_STYLE);
413                }
414                date.addClickListener(new ClickListener() {
415
416                    private static final long serialVersionUID = 1L;
417
418                    public void buttonClick(ClickEvent event) {
419
420                        if (isSelected(event.getComponent())) {
421                            resetFacetsAndSearch();
422                        } else {
423                            selectRangeFacet(
424                                CmsSimpleSearchConfigurationParser.FIELD_DATE_FACET_NAME,
425                                value.getValue());
426                        }
427                    }
428                });
429                int targetColumn;
430                int targetRow;
431                if (month < 6) {
432                    targetColumn = month;
433                    targetRow = row;
434                } else {
435                    targetColumn = month - 6;
436                    targetRow = row + 1;
437                    dateLayout.setRows(row + 2);
438                }
439                dateLayout.addComponent(date, targetColumn, targetRow);
440            }
441            Panel datePanel = new Panel(CmsVaadinUtils.getMessageText(Messages.GUI_LISTMANAGER_FACET_DATE_0));
442            datePanel.setContent(dateLayout);
443            return datePanel;
444        } else {
445            return null;
446        }
447    }
448
449    /**
450     * Prepares the folder facets for the given search result.<p>
451     *
452     * @param solrResultList the search result list
453     * @param checkedFolderFacets checked facets map
454     * @param cms the CMS context
455     *
456     * @return the folder facets component
457     */
458    private Component prepareFolderFacets(
459        CmsSolrResultList solrResultList,
460        Map<String, Boolean> checkedFolderFacets,
461        CmsObject cms) {
462
463        FacetField folderFacets = solrResultList.getFacetField(CmsSimpleSearchConfigurationParser.FIELD_PARENT_FOLDERS);
464        if ((folderFacets != null) && (folderFacets.getValueCount() > 0)) {
465            VerticalLayout folderLayout = new VerticalLayout();
466            for (final Count value : filterFolderFacets(folderFacets.getValues(), cms)) {
467                Button folder = new Button(getFolderLabel(value.getName()) + " (" + value.getCount() + ")");
468                folder.addStyleName(ValoTheme.BUTTON_TINY);
469                folder.addStyleName(ValoTheme.BUTTON_BORDERLESS);
470                Boolean selected = checkedFolderFacets.get(value.getName());
471                if ((selected != null) && selected.booleanValue()) {
472                    folder.addStyleName(SELECTED_STYLE);
473                }
474                folder.addClickListener(new ClickListener() {
475
476                    private static final long serialVersionUID = 1L;
477
478                    public void buttonClick(ClickEvent event) {
479
480                        if (isSelected(event.getComponent())) {
481                            resetFacetsAndSearch();
482                        } else {
483                            selectFieldFacet(CmsSimpleSearchConfigurationParser.FIELD_PARENT_FOLDERS, value.getName());
484                        }
485                    }
486                });
487                folderLayout.addComponent(folder);
488            }
489            Panel folderPanel = new Panel(CmsVaadinUtils.getMessageText(Messages.GUI_LISTMANAGER_FACET_FOLDERS_0));
490            folderPanel.setContent(folderLayout);
491            return folderPanel;
492        } else {
493            return null;
494        }
495    }
496
497}