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