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}