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