001/* 002 * File : $Source$ 003 * Date : $Date$ 004 * Version: $Revision$ 005 * 006 * This library is part of OpenCms - 007 * the Open Source Content Management System 008 * 009 * Copyright (C) 2002 - 2008 Alkacon Software (http://www.alkacon.com) 010 * 011 * This library is free software; you can redistribute it and/or 012 * modify it under the terms of the GNU Lesser General Public 013 * License as published by the Free Software Foundation; either 014 * version 2.1 of the License, or (at your option) any later version. 015 * 016 * This library is distributed in the hope that it will be useful, 017 * but WITHOUT ANY WARRANTY; without even the implied warranty of 018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 * Lesser General Public License for more details. 020 * 021 * For further information about Alkacon Software, please see the 022 * company website: http://www.alkacon.com 023 * 024 * For further information about OpenCms, please see the 025 * project website: http://www.opencms.org 026 * 027 * You should have received a copy of the GNU Lesser General Public 028 * License along with this library; if not, write to the Free Software 029 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 030 */ 031 032package org.opencms.search.solr; 033 034import org.opencms.file.CmsObject; 035import org.opencms.file.CmsPropertyDefinition; 036import org.opencms.i18n.CmsEncoder; 037import org.opencms.main.OpenCms; 038import org.opencms.search.fields.CmsSearchField; 039import org.opencms.util.CmsPair; 040import org.opencms.util.CmsRequestUtil; 041import org.opencms.util.CmsStringUtil; 042 043import java.util.ArrayList; 044import java.util.Arrays; 045import java.util.Collections; 046import java.util.Date; 047import java.util.HashMap; 048import java.util.List; 049import java.util.Locale; 050import java.util.Map; 051 052import org.apache.solr.client.solrj.SolrQuery; 053import org.apache.solr.common.params.CommonParams; 054 055/** 056 * A Solr search query.<p> 057 */ 058public class CmsSolrQuery extends SolrQuery { 059 060 /** A constant to add the score field to the result documents. */ 061 public static final String ALL_RETURN_FIELDS = "*,score"; 062 063 /** The default facet date gap. */ 064 public static final String DEFAULT_FACET_DATE_GAP = "+1DAY"; 065 066 /** The default query. */ 067 public static final String DEFAULT_QUERY = "*:*"; 068 069 /** The query type. */ 070 public static final String DEFAULT_QUERY_TYPE = "edismax"; 071 072 /** The default search result count. */ 073 public static final Integer DEFAULT_ROWS = new Integer(10); 074 075 /** A constant to add the score field to the result documents. */ 076 public static final String MINIMUM_FIELDS = CmsSearchField.FIELD_PATH 077 + "," 078 + CmsSearchField.FIELD_TYPE 079 + "," 080 + CmsSearchField.FIELD_SOLR_ID 081 + "," 082 + CmsSearchField.FIELD_ID; 083 084 /** A constant to add the score field to the result documents. */ 085 public static final String STRUCTURE_FIELDS = CmsSearchField.FIELD_PATH 086 + "," 087 + CmsSearchField.FIELD_TYPE 088 + "," 089 + CmsSearchField.FIELD_ID 090 + "," 091 + CmsSearchField.FIELD_CATEGORY 092 + "," 093 + CmsSearchField.FIELD_DATE_CONTENT 094 + "," 095 + CmsSearchField.FIELD_DATE_CREATED 096 + "," 097 + CmsSearchField.FIELD_DATE_EXPIRED 098 + "," 099 + CmsSearchField.FIELD_DATE_LASTMODIFIED 100 + "," 101 + CmsSearchField.FIELD_DATE_RELEASED 102 + "," 103 + CmsSearchField.FIELD_SUFFIX 104 + "," 105 + CmsSearchField.FIELD_DEPENDENCY_TYPE 106 + "," 107 + CmsSearchField.FIELD_DESCRIPTION 108 + "," 109 + CmsPropertyDefinition.PROPERTY_TITLE 110 + CmsSearchField.FIELD_DYNAMIC_PROPERTIES 111 + "," 112 + CmsSearchField.FIELD_RESOURCE_LOCALES 113 + "," 114 + CmsSearchField.FIELD_CONTENT_LOCALES 115 + "," 116 + CmsSearchField.FIELD_SCORE 117 + "," 118 + CmsSearchField.FIELD_PARENT_FOLDERS; 119 120 /** The serial version UID. */ 121 private static final long serialVersionUID = -2387357736597627703L; 122 123 /** The facet date gap to use for date facets. */ 124 private String m_facetDateGap = DEFAULT_FACET_DATE_GAP; 125 126 /** Ignore expiration flag. */ 127 private boolean m_ignoreExpiration; 128 129 /** The parameters given by the 'query string'. */ 130 private Map<String, String[]> m_queryParameters = new HashMap<String, String[]>(); 131 132 /** The search words. */ 133 private String m_text; 134 135 /** The name of the field to search the text in. */ 136 private List<String> m_textSearchFields = new ArrayList<String>(); 137 138 /** 139 * Default constructor.<p> 140 */ 141 public CmsSolrQuery() { 142 143 this(null, null); 144 } 145 146 /** 147 * Public constructor.<p> 148 * 149 * @param cms the current OpenCms context 150 * @param queryParams the Solr query parameters 151 */ 152 public CmsSolrQuery(CmsObject cms, Map<String, String[]> queryParams) { 153 154 setQuery(DEFAULT_QUERY); 155 setFields(ALL_RETURN_FIELDS); 156 setRequestHandler(DEFAULT_QUERY_TYPE); 157 setRows(DEFAULT_ROWS); 158 159 // set the values from the request context 160 if (cms != null) { 161 setLocales(Collections.singletonList(cms.getRequestContext().getLocale())); 162 setSearchRoots(Collections.singletonList(cms.getRequestContext().getSiteRoot() + "/")); 163 } 164 if (queryParams != null) { 165 m_queryParameters = queryParams; 166 } 167 ensureParameters(); 168 ensureReturnFields(); 169 ensureExpiration(); 170 } 171 172 /** 173 * Returns the resource type if only one is set as filter query.<p> 174 * 175 * @param fqs the field queries to check 176 * 177 * @return the type or <code>null</code> 178 */ 179 public static String getResourceType(String[] fqs) { 180 181 String ret = null; 182 int count = 0; 183 if (fqs != null) { 184 for (String fq : fqs) { 185 if (fq.startsWith(CmsSearchField.FIELD_TYPE + ":")) { 186 String val = fq.substring((CmsSearchField.FIELD_TYPE + ":").length()); 187 val = val.replaceAll("\"", ""); 188 if (OpenCms.getResourceManager().hasResourceType(val)) { 189 count++; 190 ret = val; 191 } 192 } 193 } 194 } 195 return (count == 1) ? ret : null; 196 } 197 198 /** 199 * Creates and adds a filter query.<p> 200 * 201 * @param fieldName the field name to create a filter query on 202 * @param vals the values that should match for the given field 203 * @param all <code>true</code> to combine the given values with 'AND', <code>false</code> for 'OR' 204 * @param useQuotes <code>true</code> to surround the given values with double quotes, <code>false</code> otherwise 205 */ 206 public void addFilterQuery(String fieldName, List<String> vals, boolean all, boolean useQuotes) { 207 208 if (getFilterQueries() != null) { 209 for (String fq : getFilterQueries()) { 210 if (fq.startsWith(fieldName + ":")) { 211 removeFilterQuery(fq); 212 } 213 } 214 } 215 addFilterQuery(createFilterQuery(fieldName, vals, all, useQuotes)); 216 } 217 218 /** 219 * Adds the given fields/orders to the existing sort fields.<p> 220 * 221 * @param sortFields the sortFields to set 222 */ 223 public void addSortFieldOrders(Map<String, ORDER> sortFields) { 224 225 if ((sortFields != null) && !sortFields.isEmpty()) { 226 // add the sort fields to the query 227 for (Map.Entry<String, ORDER> entry : sortFields.entrySet()) { 228 addSort(entry.getKey(), entry.getValue()); 229 } 230 } 231 } 232 233 /** 234 * @see java.lang.Object#clone() 235 */ 236 @Override 237 public CmsSolrQuery clone() { 238 239 CmsSolrQuery sq = new CmsSolrQuery(null, CmsRequestUtil.createParameterMap(toString())); 240 if (m_ignoreExpiration) { 241 sq.removeExpiration(); 242 } 243 return sq; 244 } 245 246 /** 247 * Ensures that the initial request parameters will overwrite the member values.<p> 248 * 249 * You can initialize the query with an HTTP request parameter then make some method calls 250 * and finally re-ensure that the initial request parameters will overwrite the changes 251 * made in the meanwhile.<p> 252 */ 253 public void ensureParameters() { 254 255 // overwrite already set values with values from query String 256 if ((m_queryParameters != null) && !m_queryParameters.isEmpty()) { 257 for (Map.Entry<String, String[]> entry : m_queryParameters.entrySet()) { 258 if (!entry.getKey().equals(CommonParams.FQ)) { 259 // add or replace all parameters from the query String 260 setParam(entry.getKey(), entry.getValue()); 261 } else { 262 // special handling for filter queries 263 replaceFilterQueries(entry.getValue()); 264 } 265 } 266 } 267 } 268 269 /** 270 * Removes the expiration flag. 271 */ 272 public void removeExpiration() { 273 274 if (getFilterQueries() != null) { 275 for (String fq : getFilterQueries()) { 276 if (fq.startsWith(CmsSearchField.FIELD_DATE_EXPIRED + ":") 277 || fq.startsWith(CmsSearchField.FIELD_DATE_RELEASED + ":")) { 278 removeFilterQuery(fq); 279 } 280 } 281 } 282 m_ignoreExpiration = true; 283 } 284 285 /** 286 * Sets the categories only if not set in the query parameters.<p> 287 * 288 * @param categories the categories to set 289 */ 290 public void setCategories(List<String> categories) { 291 292 if ((categories != null) && !categories.isEmpty()) { 293 addFilterQuery(CmsSearchField.FIELD_CATEGORY + CmsSearchField.FIELD_DYNAMIC_EXACT, categories, true, true); 294 } 295 } 296 297 /** 298 * Sets the categories only if not set in the query parameters.<p> 299 * 300 * @param categories the categories to set 301 */ 302 public void setCategories(String... categories) { 303 304 setCategories(Arrays.asList(categories)); 305 } 306 307 /** 308 * Sets date ranges.<p> 309 * 310 * This call will overwrite all existing date ranges for the given keys (name of the date facet field).<p> 311 * 312 * The parameter Map uses as:<p> 313 * <ul> 314 * <li><code>keys: </code>Solr field name {@link org.opencms.search.fields.CmsSearchField} and 315 * <li><code>values: </code> pairs with min date as first and max date as second {@link org.opencms.util.CmsPair} 316 * </ul> 317 * Alternatively you can use Solr standard query syntax like:<p> 318 * <ul> 319 * <li><code>+created:[* TO NOW]</code> 320 * <li><code>+lastmodified:[' + date + ' TO NOW]</code> 321 * </ul> 322 * whereby date is Solr formatted: 323 * {@link org.opencms.search.CmsSearchUtil#getDateAsIso8601(Date)} 324 * <p> 325 * 326 * @param dateRanges the ranges map with field name as key and a CmsPair with min date as first and max date as second 327 */ 328 public void setDateRanges(Map<String, CmsPair<Date, Date>> dateRanges) { 329 330 if ((dateRanges != null) && !dateRanges.isEmpty()) { 331 // remove the date ranges 332 for (Map.Entry<String, CmsPair<Date, Date>> entry : dateRanges.entrySet()) { 333 removeFacetField(entry.getKey()); 334 } 335 // add the date ranges 336 for (Map.Entry<String, CmsPair<Date, Date>> entry : dateRanges.entrySet()) { 337 addDateRangeFacet( 338 entry.getKey(), 339 entry.getValue().getFirst(), 340 entry.getValue().getSecond(), 341 m_facetDateGap); 342 } 343 } 344 } 345 346 /** 347 * Sets the facetDateGap.<p> 348 * 349 * @param facetDateGap the facetDateGap to set 350 */ 351 public void setFacetDateGap(String facetDateGap) { 352 353 m_facetDateGap = facetDateGap; 354 } 355 356 /** 357 * Sets the highlightFields.<p> 358 * 359 * @param highlightFields the highlightFields to set 360 */ 361 public void setHighlightFields(List<String> highlightFields) { 362 363 setParam("hl.fl", CmsStringUtil.listAsString(highlightFields, ",")); 364 } 365 366 /** 367 * Sets the highlightFields.<p> 368 * 369 * @param highlightFields the highlightFields to set 370 */ 371 public void setHighlightFields(String... highlightFields) { 372 373 setParam("hl.fl", CmsStringUtil.arrayAsString(highlightFields, ",")); 374 } 375 376 /** 377 * Sets the locales only if not set in the query parameters.<p> 378 * 379 * @param locales the locales to set 380 */ 381 public void setLocales(List<Locale> locales) { 382 383 m_textSearchFields = new ArrayList<String>(); 384 if ((locales == null) || locales.isEmpty()) { 385 m_textSearchFields.add(CmsSearchField.FIELD_TEXT); 386 if (getFilterQueries() != null) { 387 for (String fq : getFilterQueries()) { 388 if (fq.startsWith(CmsSearchField.FIELD_CONTENT_LOCALES + ":")) { 389 removeFilterQuery(fq); 390 } 391 } 392 } 393 } else { 394 List<String> localeStrings = new ArrayList<String>(); 395 for (Locale locale : locales) { 396 localeStrings.add(locale.toString()); 397 if (!m_textSearchFields.contains("text") 398 && !OpenCms.getLocaleManager().getAvailableLocales().contains(locale)) { 399 // if the locale is not configured in the opencms-system.xml 400 // there will no localized text fields, so take the general one 401 m_textSearchFields.add("text"); 402 } else { 403 m_textSearchFields.add("text_" + locale); 404 } 405 } 406 addFilterQuery(CmsSearchField.FIELD_CONTENT_LOCALES, localeStrings, false, false); 407 } 408 if (m_text != null) { 409 setText(m_text); 410 } 411 } 412 413 /** 414 * Sets the locales only if not set in the query parameters.<p> 415 * 416 * @param locales the locales to set 417 */ 418 public void setLocales(Locale... locales) { 419 420 setLocales(Arrays.asList(locales)); 421 } 422 423 /** 424 * @see org.apache.solr.client.solrj.SolrQuery#setRequestHandler(java.lang.String) 425 */ 426 @Override 427 public SolrQuery setRequestHandler(String qt) { 428 429 SolrQuery q = super.setRequestHandler(qt); 430 if (m_text != null) { 431 setText(m_text); 432 } 433 return q; 434 } 435 436 /** 437 * Sets the resource types only if not set in the query parameters.<p> 438 * 439 * @param resourceTypes the resourceTypes to set 440 */ 441 public void setResourceTypes(List<String> resourceTypes) { 442 443 if ((resourceTypes != null) && !resourceTypes.isEmpty()) { 444 addFilterQuery(CmsSearchField.FIELD_TYPE, resourceTypes, false, false); 445 } 446 } 447 448 /** 449 * Sets the resource types only if not set in the query parameters.<p> 450 * 451 * @param resourceTypes the resourceTypes to set 452 */ 453 public void setResourceTypes(String... resourceTypes) { 454 455 setResourceTypes(Arrays.asList(resourceTypes)); 456 } 457 458 /** 459 * Sets the requested return fields, but ensures that at least the 'path' and the 'type', 'id' and 'solr_id' 460 * are part of the fields returned field list.<p> 461 * 462 * @param returnFields the really requested return fields. 463 * 464 * @see CommonParams#FL 465 */ 466 public void setReturnFields(String returnFields) { 467 468 ensureReturnFields(new String[] {returnFields}); 469 } 470 471 /** 472 * Sets the search roots only if not set as query parameter.<p> 473 * 474 * @param searchRoots the searchRoots to set 475 */ 476 public void setSearchRoots(List<String> searchRoots) { 477 478 if ((searchRoots != null) && !searchRoots.isEmpty()) { 479 addFilterQuery(CmsSearchField.FIELD_PARENT_FOLDERS, searchRoots, false, true); 480 } 481 } 482 483 /** 484 * Sets the search roots only if not set as query parameter.<p> 485 * 486 * @param searchRoots the searchRoots to set 487 */ 488 public void setSearchRoots(String... searchRoots) { 489 490 setSearchRoots(Arrays.asList(searchRoots)); 491 } 492 493 /** 494 * Sets the return fields 'fl' to a predefined set that does not contain content specific fields.<p> 495 * 496 * @param structureQuery the <code>true</code> to return only structural fields 497 */ 498 public void setStructureQuery(boolean structureQuery) { 499 500 if (structureQuery) { 501 setFields(STRUCTURE_FIELDS); 502 } 503 } 504 505 /** 506 * Sets the text.<p> 507 * 508 * @param text the text to set 509 */ 510 public void setText(String text) { 511 512 m_text = text; 513 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(text)) { 514 setQuery(createTextQuery(text)); 515 } 516 } 517 518 /** 519 * Sets the textSearchFields.<p> 520 * 521 * @param textSearchFields the textSearchFields to set 522 */ 523 public void setTextSearchFields(List<String> textSearchFields) { 524 525 m_textSearchFields = textSearchFields; 526 if (m_text != null) { 527 setText(m_text); 528 } 529 } 530 531 /** 532 * Sets the textSearchFields.<p> 533 * 534 * @param textSearchFields the textSearchFields to set 535 */ 536 public void setTextSearchFields(String... textSearchFields) { 537 538 setTextSearchFields(Arrays.asList(textSearchFields)); 539 } 540 541 /** 542 * @see org.apache.solr.common.params.ModifiableSolrParams#toString() 543 */ 544 @Override 545 public String toString() { 546 547 return CmsEncoder.decode(super.toString()); 548 } 549 550 /** 551 * Creates a filter query on the given field name.<p> 552 * 553 * Creates and adds a filter query.<p> 554 * 555 * @param fieldName the field name to create a filter query on 556 * @param vals the values that should match for the given field 557 * @param all <code>true</code> to combine the given values with 'AND', <code>false</code> for 'OR' 558 * @param useQuotes <code>true</code> to surround the given values with double quotes, <code>false</code> otherwise 559 * 560 * @return a filter query String e.g. <code>fq=fieldname:val1</code> 561 */ 562 private String createFilterQuery(String fieldName, List<String> vals, boolean all, boolean useQuotes) { 563 564 String filterQuery = null; 565 if ((vals != null)) { 566 if (vals.size() == 1) { 567 if (useQuotes) { 568 filterQuery = fieldName + ":" + "\"" + vals.get(0) + "\""; 569 } else { 570 filterQuery = fieldName + ":" + vals.get(0); 571 } 572 } else if (vals.size() > 1) { 573 filterQuery = fieldName + ":("; 574 for (int j = 0; j < vals.size(); j++) { 575 String val; 576 if (useQuotes) { 577 val = "\"" + vals.get(j) + "\""; 578 } else { 579 val = vals.get(j); 580 } 581 filterQuery += val; 582 if (vals.size() > (j + 1)) { 583 if (all) { 584 filterQuery += " AND "; 585 } else { 586 filterQuery += " OR "; 587 } 588 } 589 } 590 filterQuery += ")"; 591 } 592 } 593 return filterQuery; 594 } 595 596 /** 597 * Creates a OR combined 'q' parameter.<p> 598 * 599 * @param text the query string. 600 * 601 * @return returns the 'q' parameter 602 */ 603 private String createTextQuery(String text) { 604 605 if (m_textSearchFields.isEmpty()) { 606 m_textSearchFields.add(CmsSearchField.FIELD_TEXT); 607 } 608 String q = "{!q.op=OR type=" + getRequestHandler() + " qf="; 609 boolean first = true; 610 for (String textField : m_textSearchFields) { 611 if (!first) { 612 q += " "; 613 } 614 q += textField; 615 } 616 q += "}" + text; 617 return q; 618 } 619 620 /** 621 * Ensures that expired and not yet released resources are not returned by default.<p> 622 */ 623 private void ensureExpiration() { 624 625 boolean expirationDateSet = false; 626 boolean releaseDateSet = false; 627 if (getFilterQueries() != null) { 628 for (String fq : getFilterQueries()) { 629 if (fq.startsWith(CmsSearchField.FIELD_DATE_EXPIRED + ":")) { 630 expirationDateSet = true; 631 } 632 if (fq.startsWith(CmsSearchField.FIELD_DATE_RELEASED + ":")) { 633 releaseDateSet = true; 634 } 635 } 636 } 637 if (!expirationDateSet) { 638 addFilterQuery(CmsSearchField.FIELD_DATE_EXPIRED + ":[NOW TO *]"); 639 } 640 if (!releaseDateSet) { 641 addFilterQuery(CmsSearchField.FIELD_DATE_RELEASED + ":[* TO NOW]"); 642 } 643 } 644 645 /** 646 * Ensures that at least the 'path' and the 'type', 'id' and 'solr_id' are part of the fields returned field list.<p> 647 * 648 * @see CommonParams#FL 649 */ 650 private void ensureReturnFields() { 651 652 ensureReturnFields(getParams(CommonParams.FL)); 653 } 654 655 /** 656 * Ensures that at least the 'path' and the 'type', 'id' and 'solr_id' are part of the fields returned field list.<p> 657 * 658 * @param requestedReturnFields the really requested return fields. 659 * 660 * @see CommonParams#FL 661 */ 662 private void ensureReturnFields(String[] requestedReturnFields) { 663 664 if ((requestedReturnFields != null) && (requestedReturnFields.length > 0)) { 665 List<String> result = new ArrayList<String>(); 666 for (String field : requestedReturnFields) { 667 String commasep = field.replaceAll(" ", ","); 668 List<String> list = CmsStringUtil.splitAsList(commasep, ','); 669 if (!list.contains("*")) { 670 for (String reqField : CmsStringUtil.splitAsList(MINIMUM_FIELDS, ",")) { 671 if (!list.contains(reqField)) { 672 list.add(reqField); 673 } 674 } 675 } 676 result.addAll(list); 677 } 678 setParam(CommonParams.FL, CmsStringUtil.arrayAsString(result.toArray(new String[0]), ",")); 679 } 680 } 681 682 /** 683 * Removes those filter queries that restrict the fields used in the given filter query Strings.<p> 684 * 685 * Searches in the given Strings for a ":", then takes the field name part 686 * and removes the already set filter queries queries that are matching the same field name.<p> 687 * 688 * @param fqs the filter query Strings in the format <code>fq=fieldname:value</code> that should be removed 689 */ 690 private void removeFilterQueries(String[] fqs) { 691 692 // iterate over the given filter queries to remove 693 for (String fq : fqs) { 694 int idx = fq.indexOf(':'); 695 if (idx != -1) { 696 // get the field name of the fq to remove 697 String fieldName = fq.substring(0, idx); 698 // iterate over the fqs of the already existing fqs from the solr query 699 if (getFilterQueries() != null) { 700 for (String sfq : getFilterQueries()) { 701 if (sfq.startsWith(fieldName + ":")) { 702 // there exists a filter query for exact the same field, remove it 703 removeFilterQuery(sfq); 704 } 705 } 706 } 707 } 708 } 709 } 710 711 /** 712 * Removes the given filter queries, if already set and then adds the filter queries again.<p> 713 * 714 * @param fqs the filter queries to remove 715 */ 716 private void replaceFilterQueries(String[] fqs) { 717 718 removeFilterQueries(fqs); 719 addFilterQuery(fqs); 720 } 721}