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.config.parser; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsPropertyDefinition; 032import org.opencms.json.JSONException; 033import org.opencms.jsp.search.config.CmsSearchConfigurationFacetField; 034import org.opencms.jsp.search.config.CmsSearchConfigurationFacetRange; 035import org.opencms.jsp.search.config.CmsSearchConfigurationSortOption; 036import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacet.SortOrder; 037import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetField; 038import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetRange; 039import org.opencms.jsp.search.config.I_CmsSearchConfigurationPagination; 040import org.opencms.jsp.search.config.I_CmsSearchConfigurationSortOption; 041import org.opencms.main.CmsException; 042import org.opencms.relations.CmsCategoryService; 043import org.opencms.search.fields.CmsSearchField; 044import org.opencms.search.solr.CmsSolrQuery; 045import org.opencms.ui.apps.lists.CmsListManager; 046import org.opencms.ui.apps.lists.CmsListManager.ListConfigurationBean; 047import org.opencms.ui.apps.lists.daterestrictions.I_CmsListDateRestriction; 048import org.opencms.util.CmsStringUtil; 049import org.opencms.util.CmsUUID; 050import org.opencms.xml.types.CmsXmlDisplayFormatterValue; 051 052import java.util.Collections; 053import java.util.HashMap; 054import java.util.LinkedList; 055import java.util.List; 056import java.util.Locale; 057import java.util.Map; 058import java.util.Objects; 059 060import org.apache.solr.common.params.CommonParams; 061 062import com.google.common.collect.Lists; 063 064/** 065 * Search configuration parser using a list configuration file as the base configuration with additional JSON.<p> 066 */ 067public class CmsSimpleSearchConfigurationParser extends CmsJSONSearchConfigurationParser { 068 069 /** Sort options that are available by default. */ 070 public static enum SortOption { 071 /** Sort by date ascending. */ 072 DATE_ASC, 073 /** Sort by date descending. */ 074 DATE_DESC, 075 /** Sort by title ascending. */ 076 TITLE_ASC, 077 /** Sort by title descending. */ 078 TITLE_DESC, 079 /** Sort by order ascending. */ 080 ORDER_ASC, 081 /** Sort by order descending. */ 082 ORDER_DESC; 083 084 /** 085 * Generates the suitable {@link I_CmsSearchConfigurationSortOption} for the option. 086 * @param l the locale for which the option should be created 087 * @return the created {@link I_CmsSearchConfigurationSortOption} 088 */ 089 public I_CmsSearchConfigurationSortOption getOption(Locale l) { 090 091 switch (this) { 092 case DATE_ASC: 093 return new CmsSearchConfigurationSortOption("date.asc", "date_asc", getSortDateField(l) + " asc"); 094 case DATE_DESC: 095 return new CmsSearchConfigurationSortOption( 096 "date.desc", 097 "date_desc", 098 getSortDateField(l) + " desc"); 099 case TITLE_ASC: 100 return new CmsSearchConfigurationSortOption( 101 "title.asc", 102 "title_asc", 103 getSortTitleField(l) + " asc"); 104 case TITLE_DESC: 105 return new CmsSearchConfigurationSortOption( 106 "title.desc", 107 "title_desc", 108 getSortTitleField(l) + " desc"); 109 case ORDER_ASC: 110 return new CmsSearchConfigurationSortOption( 111 "order.asc", 112 "order_asc", 113 getSortOrderField(l) + " asc"); 114 case ORDER_DESC: 115 return new CmsSearchConfigurationSortOption( 116 "order.desc", 117 "order_desc", 118 getSortOrderField(l) + " desc"); 119 default: 120 throw new IllegalArgumentException(); 121 } 122 } 123 124 /** 125 * Returns the locale specific date field to use for sorting. 126 * @param l the locale to use, can be <code>null</code> 127 * @return the locale specific date field to use for sorting. 128 */ 129 protected String getSortDateField(Locale l) { 130 131 return CmsSearchField.FIELD_INSTANCEDATE 132 + (null != l ? "_" + l.toString() : "") 133 + CmsSearchField.FIELD_POSTFIX_DATE; 134 } 135 136 /** 137 * Returns the locale specific order field to use for sorting. 138 * @param l the locale to use, can be <code>null</code> 139 * @return the locale specific order field to use for sorting. 140 */ 141 protected String getSortOrderField(Locale l) { 142 143 return CmsSearchField.FIELD_DISPORDER 144 + (null != l ? "_" + l.toString() : "") 145 + CmsSearchField.FIELD_POSTFIX_INT; 146 } 147 148 /** 149 * Returns the locale specific title field to use for sorting. 150 * @param l the locale to use, can be <code>null</code> 151 * @return the locale specific title field to use for sorting. 152 */ 153 protected String getSortTitleField(Locale l) { 154 155 return CmsSearchField.FIELD_DISPTITLE 156 + (null != l ? "_" + l.toString() : "") 157 + CmsSearchField.FIELD_POSTFIX_SORT; 158 } 159 } 160 161 /** Pagination which may override the default pagination. */ 162 private I_CmsSearchConfigurationPagination m_pagination; 163 164 /** The current cms context. */ 165 private CmsObject m_cms; 166 167 /** The list configuration bean. */ 168 private ListConfigurationBean m_config; 169 170 /** The (mutable) search locale. */ 171 private Locale m_searchLocale; 172 173 /** The (mutable) sort order. */ 174 private CmsSimpleSearchConfigurationParser.SortOption m_sortOrder; 175 176 /** Flag which, if true, causes the search to ignore the blacklist. */ 177 private boolean m_ignoreBlacklist; 178 179 /** 180 * Constructor.<p> 181 * 182 * @param cms the cms context 183 * @param config the list configuration 184 * @param additionalParamJSON the additional JSON configuration 185 * 186 * @throws JSONException in case parsing the JSON fails 187 */ 188 public CmsSimpleSearchConfigurationParser( 189 CmsObject cms, 190 CmsListManager.ListConfigurationBean config, 191 String additionalParamJSON) 192 throws JSONException { 193 194 super(CmsStringUtil.isEmptyOrWhitespaceOnly(additionalParamJSON) ? "{}" : additionalParamJSON); 195 m_cms = cms; 196 m_config = config; 197 } 198 199 /** 200 * Creates an instance for an empty JSON configuration.<p> 201 * 202 * The point of this is that we know that passing an empty configuration makes it impossible 203 * for a JSONException to thrown. 204 * 205 * @param cms the current CMS context 206 * @param config the search configuration 207 * 208 * @return the search config parser 209 */ 210 public static CmsSimpleSearchConfigurationParser createInstanceWithNoJsonConfig( 211 CmsObject cms, 212 CmsListManager.ListConfigurationBean config) { 213 214 try { 215 return new CmsSimpleSearchConfigurationParser(cms, config, null); 216 217 } catch (JSONException e) { 218 return null; 219 } 220 } 221 222 /** 223 * Returns the initial SOLR query.<p> 224 * 225 * @return the SOLR query 226 */ 227 public CmsSolrQuery getInitialQuery() { 228 229 Map<String, String[]> queryParams = new HashMap<String, String[]>(); 230 if (!m_cms.getRequestContext().getCurrentProject().isOnlineProject() && m_config.isShowExpired()) { 231 queryParams.put("fq", new String[] {"released:[* TO *]", "expired:[* TO *]"}); 232 } 233 return new CmsSolrQuery(null, queryParams); 234 } 235 236 /** 237 * Gets the search locale.<p> 238 * 239 * @return the search locale 240 */ 241 public Locale getSearchLocale() { 242 243 if (m_searchLocale != null) { 244 return m_searchLocale; 245 } 246 return m_cms.getRequestContext().getLocale(); 247 } 248 249 /** 250 * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets() 251 */ 252 @Override 253 public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() { 254 255 if (m_configObject.has(JSON_KEY_FIELD_FACETS)) { 256 return super.parseFieldFacets(); 257 } else { 258 return getDefaultFieldFacets(true); 259 } 260 } 261 262 /** 263 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#parsePagination() 264 */ 265 @Override 266 public I_CmsSearchConfigurationPagination parsePagination() { 267 268 if (m_pagination != null) { 269 return m_pagination; 270 } 271 return super.parsePagination(); 272 } 273 274 /** 275 * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets() 276 */ 277 @Override 278 public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() { 279 280 if (m_configObject.has(JSON_KEY_RANGE_FACETS)) { 281 return super.parseRangeFacets(); 282 } else { 283 Map<String, I_CmsSearchConfigurationFacetRange> rangeFacets = new HashMap<String, I_CmsSearchConfigurationFacetRange>(); 284 I_CmsSearchConfigurationFacetRange rangeFacet = new CmsSearchConfigurationFacetRange( 285 String.format(CmsListManager.FIELD_DATE, getSearchLocale().toString()), 286 "NOW/YEAR-20YEARS", 287 "NOW/MONTH+2YEARS", 288 "+1MONTHS", 289 null, 290 Boolean.FALSE, 291 CmsListManager.FIELD_DATE_FACET_NAME, 292 Integer.valueOf(1), 293 "Date", 294 Boolean.FALSE, 295 null, 296 Boolean.TRUE); 297 298 rangeFacets.put(rangeFacet.getName(), rangeFacet); 299 return rangeFacets; 300 } 301 } 302 303 /** 304 * Sets the 'ignore blacklist' flag.<p> 305 * 306 * If set, the search will ignore the blacklist from the list configuration.<p> 307 * 308 * @param ignoreBlacklist true if the blacklist should be ignored 309 */ 310 public void setIgnoreBlacklist(boolean ignoreBlacklist) { 311 312 m_ignoreBlacklist = ignoreBlacklist; 313 } 314 315 /** 316 * Sets the pagination.<p> 317 * 318 * If this is set, parsePagination will always return the set value instead of using the default way to compute the pagination 319 * 320 * @param pagination the pagination 321 */ 322 public void setPagination(I_CmsSearchConfigurationPagination pagination) { 323 324 m_pagination = pagination; 325 } 326 327 /** 328 * Sets the search locale.<p> 329 * 330 * @param locale the search locale 331 */ 332 public void setSearchLocale(Locale locale) { 333 334 m_searchLocale = locale; 335 } 336 337 /** 338 * Sets the sort option.<p> 339 * 340 * @param sortOption the sort option 341 */ 342 public void setSortOption(String sortOption) { 343 344 if (null != sortOption) { 345 try { 346 m_sortOrder = CmsSimpleSearchConfigurationParser.SortOption.valueOf(sortOption); 347 } catch (IllegalArgumentException e) { 348 m_sortOrder = null; 349 LOG.warn( 350 "Setting illegal default sort option " + sortOption + " failed. Using Solr's default sort option."); 351 } 352 } else { 353 m_sortOrder = null; 354 } 355 } 356 357 /** 358 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getEscapeQueryChars() 359 */ 360 @Override 361 protected Boolean getEscapeQueryChars() { 362 363 if (m_configObject.has(JSON_KEY_ESCAPE_QUERY_CHARACTERS)) { 364 return super.getEscapeQueryChars(); 365 } else { 366 return Boolean.TRUE; 367 } 368 } 369 370 /** 371 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getExtraSolrParams() 372 */ 373 @Override 374 protected String getExtraSolrParams() { 375 376 String params = super.getExtraSolrParams(); 377 if (CmsStringUtil.isEmptyOrWhitespaceOnly(params)) { 378 params = getFolderFilter() 379 + getResourceTypeFilter() 380 + getCategoryFilter() 381 + getFilterQuery() 382 + getBlacklistFilter(); 383 } 384 return params; 385 } 386 387 /** 388 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getIgnoreExpirationDate() 389 */ 390 @Override 391 protected Boolean getIgnoreExpirationDate() { 392 393 return getIgnoreReleaseAndExpiration(); 394 395 } 396 397 /** 398 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getIgnoreReleaseDate() 399 */ 400 @Override 401 protected Boolean getIgnoreReleaseDate() { 402 403 return getIgnoreReleaseAndExpiration(); 404 } 405 406 /** 407 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getQueryModifier() 408 */ 409 @Override 410 protected String getQueryModifier() { 411 412 String modifier = super.getQueryModifier(); 413 if (CmsStringUtil.isEmptyOrWhitespaceOnly(modifier)) { 414 modifier = "{!type=edismax qf=\"" 415 + CmsSearchField.FIELD_CONTENT 416 + "_" 417 + getSearchLocale().toString() 418 + " " 419 + CmsPropertyDefinition.PROPERTY_TITLE 420 + CmsSearchField.FIELD_DYNAMIC_PROPERTIES 421 + " " 422 + CmsPropertyDefinition.PROPERTY_DESCRIPTION 423 + CmsSearchField.FIELD_DYNAMIC_PROPERTIES_DIRECT 424 + " " 425 + CmsPropertyDefinition.PROPERTY_DESCRIPTION_HTML 426 + CmsSearchField.FIELD_DYNAMIC_PROPERTIES_DIRECT 427 + " " 428 + CmsSearchField.FIELD_SPELL 429 + "\"}%(query)"; 430 } 431 return modifier; 432 } 433 434 /** 435 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getSearchForEmptyQuery() 436 */ 437 @Override 438 protected Boolean getSearchForEmptyQuery() { 439 440 if (m_configObject.has(JSON_KEY_SEARCH_FOR_EMPTY_QUERY)) { 441 return super.getSearchForEmptyQuery(); 442 } else { 443 return Boolean.TRUE; 444 } 445 } 446 447 /** 448 * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getSortOptions() 449 */ 450 @Override 451 protected List<I_CmsSearchConfigurationSortOption> getSortOptions() { 452 453 if (m_configObject.has(JSON_KEY_SORTOPTIONS)) { 454 return super.getSortOptions(); 455 } else { 456 List<I_CmsSearchConfigurationSortOption> options = new LinkedList<I_CmsSearchConfigurationSortOption>(); 457 458 CmsSimpleSearchConfigurationParser.SortOption currentOption = CmsSimpleSearchConfigurationParser.SortOption.valueOf( 459 m_config.getSortOrder()); 460 if (m_sortOrder != null) { 461 currentOption = m_sortOrder; 462 } 463 Locale locale = getSearchLocale(); 464 options.add(currentOption.getOption(locale)); 465 CmsSimpleSearchConfigurationParser.SortOption[] sortOptions = CmsSimpleSearchConfigurationParser.SortOption.values(); 466 for (int i = 0; i < sortOptions.length; i++) { 467 CmsSimpleSearchConfigurationParser.SortOption option = sortOptions[i]; 468 if (!Objects.equals(currentOption, option)) { 469 options.add(option.getOption(locale)); 470 } 471 } 472 return options; 473 } 474 } 475 476 /** 477 * Returns the blacklist filter.<p> 478 * 479 * @return the blacklist filter 480 */ 481 String getBlacklistFilter() { 482 483 if (m_ignoreBlacklist) { 484 return ""; 485 } 486 String result = ""; 487 List<CmsUUID> blacklist = m_config.getBlacklist(); 488 List<String> blacklistStrings = Lists.newArrayList(); 489 for (CmsUUID id : blacklist) { 490 blacklistStrings.add("\"" + id.toString() + "\""); 491 } 492 if (!blacklistStrings.isEmpty()) { 493 result = "&fq=-id:(" + CmsStringUtil.listAsString(blacklistStrings, " OR ") + ")"; 494 } 495 return result; 496 } 497 498 /** 499 * Returns the category filter string.<p> 500 * 501 * @return the category filter 502 */ 503 String getCategoryFilter() { 504 505 String result = ""; 506 if (!m_config.getCategories().isEmpty()) { 507 List<String> categoryVals = Lists.newArrayList(); 508 for (String path : m_config.getCategories()) { 509 try { 510 path = CmsCategoryService.getInstance().getCategory( 511 m_cms, 512 m_cms.getRequestContext().addSiteRoot(path)).getPath(); 513 categoryVals.add("\"" + path + "\""); 514 } catch (CmsException e) { 515 LOG.warn(e.getLocalizedMessage(), e); 516 } 517 } 518 if (!categoryVals.isEmpty()) { 519 String operator = " " + m_config.getCategoryMode() + " "; 520 String valueExpression = CmsStringUtil.listAsString(categoryVals, operator); 521 result = "&fq=category_exact:(" + valueExpression + ")"; 522 523 } 524 } 525 return result; 526 } 527 528 /** 529 * The fields returned by default. Typically the output is done via display formatters and hence nearly no 530 * field is necessary. Returning all fields might cause performance problems. 531 * 532 * @return the default return fields. 533 */ 534 String getDefaultReturnFields() { 535 536 StringBuffer fields = new StringBuffer(""); 537 fields.append(CmsSearchField.FIELD_PATH); 538 fields.append(','); 539 fields.append(CmsSearchField.FIELD_INSTANCEDATE).append('_').append(getSearchLocale().toString()).append("_dt"); 540 fields.append(','); 541 fields.append(CmsSearchField.FIELD_INSTANCEDATE_END).append('_').append(getSearchLocale().toString()).append( 542 "_dt"); 543 fields.append(','); 544 fields.append(CmsSearchField.FIELD_INSTANCEDATE_CURRENT_TILL).append('_').append( 545 getSearchLocale().toString()).append("_dt"); 546 fields.append(','); 547 fields.append(CmsSearchField.FIELD_ID); 548 fields.append(','); 549 fields.append(CmsSearchField.FIELD_SOLR_ID); 550 fields.append(','); 551 fields.append(CmsSearchField.FIELD_DISPTITLE).append('_').append(getSearchLocale().toString()).append("_sort"); 552 fields.append(','); 553 fields.append(CmsSearchField.FIELD_LINK); 554 return fields.toString(); 555 } 556 557 /** 558 * Returns the filter query string.<p> 559 * 560 * @return the filter query 561 */ 562 String getFilterQuery() { 563 564 String result = m_config.getFilterQuery(); 565 if (result == null) { 566 result = ""; 567 } 568 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(result) && !result.startsWith("&")) { 569 result = "&" + result; 570 } 571 if (!result.contains(CommonParams.FL + "=")) { 572 result += "&" + CommonParams.FL + "=" + getDefaultReturnFields(); 573 } 574 I_CmsListDateRestriction dateRestriction = m_config.getDateRestriction(); 575 if (dateRestriction != null) { 576 result += "&fq=" 577 + CmsSearchField.FIELD_INSTANCEDATE_CURRENT_TILL 578 + "_" 579 + getSearchLocale().toString() 580 + "_dt:" 581 + dateRestriction.getRange(); 582 583 } 584 return result; 585 } 586 587 /** 588 * Returns the folder filter string.<p> 589 * 590 * @return the folder filter 591 */ 592 String getFolderFilter() { 593 594 String result = ""; 595 List<String> parentFolderVals = Lists.newArrayList(); 596 if (!m_config.getFolders().isEmpty()) { 597 for (String value : m_config.getFolders()) { 598 parentFolderVals.add("\"" + value + "\""); 599 } 600 } 601 if (parentFolderVals.isEmpty()) { 602 result = "fq=parent-folders:(\"/\")"; 603 } else { 604 result = "fq=parent-folders:(" + CmsStringUtil.listAsString(parentFolderVals, " OR ") + ")"; 605 } 606 return result; 607 } 608 609 /** 610 * Returns the resource type filter string.<p> 611 * 612 * @return the folder filter 613 */ 614 String getResourceTypeFilter() { 615 616 String result = ""; 617 List<String> typeVals = Lists.newArrayList(); 618 if (!m_config.getDisplayTypes().isEmpty()) { 619 for (String displayType : m_config.getDisplayTypes()) { 620 if (displayType.contains(CmsXmlDisplayFormatterValue.SEPARATOR)) { 621 displayType = displayType.substring(0, displayType.indexOf(CmsXmlDisplayFormatterValue.SEPARATOR)); 622 } 623 typeVals.add("\"" + displayType + "\""); 624 } 625 } 626 if (!typeVals.isEmpty()) { 627 result = "&fq=type:(" + CmsStringUtil.listAsString(typeVals, " OR ") + ")"; 628 } 629 return result; 630 } 631 632 /** The default field facets. 633 * 634 * @param categoryConjunction flag, indicating if category selections in the facet should be "AND" combined. 635 * @return the default field facets. 636 */ 637 private Map<String, I_CmsSearchConfigurationFacetField> getDefaultFieldFacets(boolean categoryConjunction) { 638 639 Map<String, I_CmsSearchConfigurationFacetField> fieldFacets = new HashMap<String, I_CmsSearchConfigurationFacetField>(); 640 fieldFacets.put( 641 CmsListManager.FIELD_CATEGORIES, 642 new CmsSearchConfigurationFacetField( 643 CmsListManager.FIELD_CATEGORIES, 644 null, 645 Integer.valueOf(1), 646 Integer.valueOf(200), 647 null, 648 "Category", 649 SortOrder.index, 650 null, 651 Boolean.valueOf(categoryConjunction), 652 null, 653 Boolean.TRUE)); 654 fieldFacets.put( 655 CmsListManager.FIELD_PARENT_FOLDERS, 656 new CmsSearchConfigurationFacetField( 657 CmsListManager.FIELD_PARENT_FOLDERS, 658 null, 659 Integer.valueOf(1), 660 Integer.valueOf(200), 661 null, 662 "Folders", 663 SortOrder.index, 664 null, 665 Boolean.FALSE, 666 null, 667 Boolean.TRUE)); 668 return Collections.unmodifiableMap(fieldFacets); 669 670 } 671 672 /** 673 * Returns a flag, indicating if the release and expiration date should be ignored. 674 * @return a flag, indicating if the release and expiration date should be ignored. 675 */ 676 private Boolean getIgnoreReleaseAndExpiration() { 677 678 return Boolean.valueOf(m_config.isShowExpired()); 679 } 680}