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