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 - 2009 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; 033 034import org.opencms.configuration.CmsParameterConfiguration; 035import org.opencms.file.CmsObject; 036import org.opencms.file.CmsResource; 037import org.opencms.file.CmsResourceFilter; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsIllegalArgumentException; 040import org.opencms.main.CmsLog; 041import org.opencms.main.OpenCms; 042import org.opencms.report.I_CmsReport; 043import org.opencms.search.documents.I_CmsTermHighlighter; 044import org.opencms.search.extractors.CmsExtractionResult; 045import org.opencms.search.extractors.I_CmsExtractionResult; 046import org.opencms.search.fields.CmsLuceneFieldConfiguration; 047import org.opencms.search.fields.CmsSearchField; 048import org.opencms.search.fields.CmsSearchFieldConfiguration; 049import org.opencms.util.CmsFileUtil; 050import org.opencms.util.CmsStringUtil; 051 052import java.io.File; 053import java.io.IOException; 054import java.nio.file.Paths; 055import java.text.ParseException; 056import java.util.ArrayList; 057import java.util.Calendar; 058import java.util.Collections; 059import java.util.Date; 060import java.util.HashMap; 061import java.util.List; 062import java.util.Locale; 063import java.util.Map; 064import java.util.Set; 065 066import org.apache.commons.logging.Log; 067import org.apache.lucene.analysis.Analyzer; 068import org.apache.lucene.document.DateTools; 069import org.apache.lucene.document.Document; 070import org.apache.lucene.index.DirectoryReader; 071import org.apache.lucene.index.FieldInfo; 072import org.apache.lucene.index.IndexReader; 073import org.apache.lucene.index.IndexWriter; 074import org.apache.lucene.index.IndexWriterConfig; 075import org.apache.lucene.index.StoredFieldVisitor; 076import org.apache.lucene.index.Term; 077import org.apache.lucene.queryparser.classic.QueryParser; 078import org.apache.lucene.search.BooleanClause; 079import org.apache.lucene.search.BooleanClause.Occur; 080import org.apache.lucene.search.BooleanQuery; 081import org.apache.lucene.search.IndexSearcher; 082import org.apache.lucene.search.MatchAllDocsQuery; 083import org.apache.lucene.search.MultiTermQuery; 084import org.apache.lucene.search.Query; 085import org.apache.lucene.search.ScoreMode; 086import org.apache.lucene.search.Sort; 087import org.apache.lucene.search.SortField; 088import org.apache.lucene.search.TermQuery; 089import org.apache.lucene.search.TopDocs; 090import org.apache.lucene.search.similarities.Similarity; 091import org.apache.lucene.store.Directory; 092import org.apache.lucene.store.FSDirectory; 093import org.apache.lucene.store.IOContext; 094import org.apache.solr.uninverting.UninvertingReader; 095import org.apache.solr.uninverting.UninvertingReader.Type; 096 097/** 098 * Abstract search index implementation.<p> 099 */ 100public class CmsSearchIndex extends A_CmsSearchIndex { 101 102 /** A constant for the full qualified name of the CmsSearchIndex class. */ 103 public static final String A_PARAM_PREFIX = "org.opencms.search.CmsSearchIndex"; 104 105 /** Constant for additional parameter to enable optimized full index regeneration (default: false). */ 106 public static final String BACKUP_REINDEXING = A_PARAM_PREFIX + ".useBackupReindexing"; 107 108 /** Look table to quickly zero-pad days / months in date Strings. */ 109 public static final String[] DATES = new String[] { 110 "00", 111 "01", 112 "02", 113 "03", 114 "04", 115 "05", 116 "06", 117 "07", 118 "08", 119 "09", 120 "10", 121 "11", 122 "12", 123 "13", 124 "14", 125 "15", 126 "16", 127 "17", 128 "18", 129 "19", 130 "20", 131 "21", 132 "22", 133 "23", 134 "24", 135 "25", 136 "26", 137 "27", 138 "28", 139 "29", 140 "30", 141 "31"}; 142 143 /** Constant for a field list that contains the "meta" field as well as the "content" field. */ 144 public static final String[] DOC_META_FIELDS = new String[] { 145 CmsSearchField.FIELD_META, 146 CmsSearchField.FIELD_CONTENT}; 147 148 /** Constant for additional parameter to enable excerpt creation (default: true). */ 149 public static final String EXCERPT = A_PARAM_PREFIX + ".createExcerpt"; 150 151 /** Constant for additional parameter for index content extraction. */ 152 public static final String EXTRACT_CONTENT = A_PARAM_PREFIX + ".extractContent"; 153 154 /** Constant for additional parameter to enable/disable language detection (default: false). */ 155 public static final String IGNORE_EXPIRATION = A_PARAM_PREFIX + ".ignoreExpiration"; 156 157 /** Constant for additional parameter to enable/disable language detection (default: false). */ 158 public static final String LANGUAGEDETECTION = "search.solr.useLanguageDetection"; 159 160 /** Constant for additional parameter for the Lucene index setting. */ 161 public static final String LUCENE_AUTO_COMMIT = "lucene.AutoCommit"; 162 163 /** Constant for additional parameter for the Lucene index setting. */ 164 public static final String LUCENE_RAM_BUFFER_SIZE_MB = "lucene.RAMBufferSizeMB"; 165 166 /** Constant for additional parameter for controlling how many hits are loaded at maximum (default: 1000). */ 167 public static final String MAX_HITS = A_PARAM_PREFIX + ".maxHits"; 168 169 /** Indicates how many hits are loaded at maximum by default. */ 170 public static final int MAX_HITS_DEFAULT = 5000; 171 172 /** Constant for years max range span in document search. */ 173 public static final int MAX_YEAR_RANGE = 25; 174 175 /** Constant for additional parameter to enable permission checks (default: true). */ 176 public static final String PERMISSIONS = A_PARAM_PREFIX + ".checkPermissions"; 177 178 /** Constant for additional parameter to set the thread priority during search. */ 179 public static final String PRIORITY = A_PARAM_PREFIX + ".priority"; 180 181 /** Constant for additional parameter to enable time range checks (default: true). */ 182 public static final String TIME_RANGE = A_PARAM_PREFIX + ".checkTimeRange"; 183 184 /** 185 * A stored field visitor, that does not return the large fields: "content" and "contentblob".<p> 186 */ 187 protected static final StoredFieldVisitor VISITOR = new StoredFieldVisitor() { 188 189 /** 190 * @see org.apache.lucene.index.StoredFieldVisitor#needsField(org.apache.lucene.index.FieldInfo) 191 */ 192 @Override 193 public Status needsField(FieldInfo fieldInfo) { 194 195 return !CmsSearchFieldConfiguration.LAZY_FIELDS.contains(fieldInfo.name) ? Status.YES : Status.NO; 196 } 197 }; 198 199 /** The log object for this class. */ 200 private static final Log LOG = CmsLog.getLog(CmsSearchIndex.class); 201 202 /** The serial version id. */ 203 private static final long serialVersionUID = 8461682478204452718L; 204 205 /** The configured Lucene analyzer used for this index. */ 206 private transient Analyzer m_analyzer; 207 208 /** Indicates if backup re-indexing is used by this index. */ 209 private boolean m_backupReindexing; 210 211 /** The permission check mode for this index. */ 212 private boolean m_checkPermissions; 213 214 /** The time range check mode for this index. */ 215 private boolean m_checkTimeRange; 216 217 /** The excerpt mode for this index. */ 218 private boolean m_createExcerpt; 219 220 /** Map of display query filters to use. */ 221 private transient Map<String, Query> m_displayFilters; 222 223 /** 224 * Signals whether expiration dates should be ignored when checking permissions or not.<p> 225 * @see #IGNORE_EXPIRATION 226 */ 227 private boolean m_ignoreExpiration; 228 229 /** The Lucene index searcher to use. */ 230 private transient IndexSearcher m_indexSearcher; 231 232 /** The Lucene index RAM buffer size, see {@link IndexWriterConfig#setRAMBufferSizeMB(double)}. */ 233 private Double m_luceneRAMBufferSizeMB; 234 235 /** Indicates how many hits are loaded at maximum. */ 236 private int m_maxHits; 237 238 /** The thread priority for a search. */ 239 private int m_priority; 240 241 /** Controls if a resource requires view permission to be displayed in the result list. */ 242 private boolean m_requireViewPermission; 243 244 /** The cms specific Similarity implementation. */ 245 private final transient Similarity m_sim = new CmsSearchSimilarity(); 246 247 /** 248 * Default constructor only intended to be used by the XML configuration. <p> 249 * 250 * It is recommended to use the constructor <code>{@link #CmsSearchIndex(String)}</code> 251 * as it enforces the mandatory name argument. <p> 252 */ 253 public CmsSearchIndex() { 254 255 super(); 256 m_checkPermissions = true; 257 m_priority = -1; 258 m_createExcerpt = true; 259 m_maxHits = MAX_HITS_DEFAULT; 260 m_checkTimeRange = false; 261 } 262 263 /** 264 * Creates a new CmsSearchIndex with the given name.<p> 265 * 266 * @param name the system-wide unique name for the search index 267 * 268 * @throws CmsIllegalArgumentException if the given name is null, empty or already taken by another search index 269 */ 270 public CmsSearchIndex(String name) 271 throws CmsIllegalArgumentException { 272 273 this(); 274 setName(name); 275 } 276 277 /** 278 * Generates a list of date terms for the optimized date range search with "daily" granularity level.<p> 279 * 280 * How this works:<ul> 281 * <li>For each document, terms are added for the year, the month and the day the document 282 * was modified or created) in. So for example if a document is modified at February 02, 2009, 283 * then the following terms are stored for this document: 284 * "20090202", "200902" and "2009".</li> 285 * <li>In case a date range search is done, then all possible matches for the 286 * provided rage are created as search terms and matched with the document terms.</li> 287 * <li>Consider the following use case: You want to find out if a resource has been changed 288 * in the time between November 29, 2007 and March 01, 2009. 289 * One term to match is simply "2008" because if a document 290 * was modified in 2008, then it is clearly in the date range. 291 * Other terms are "200712", "200901" and "200902", because all documents 292 * modified in these months are also a certain matches. 293 * Finally we need to add terms for "20071129", "20071130" and "20090301" to match the days in the 294 * starting and final month.</li> 295 * </ul> 296 * 297 * @param startDate start date of the range to search in 298 * @param endDate end date of the range to search in 299 * 300 * @return a list of date terms for the optimized date range search 301 */ 302 public static List<String> getDateRangeSpan(long startDate, long endDate) { 303 304 if (startDate > endDate) { 305 // switch so that the end is always before the start 306 long temp = endDate; 307 endDate = startDate; 308 startDate = temp; 309 } 310 311 List<String> result = new ArrayList<String>(100); 312 313 // initialize calendars from the time value 314 Calendar calStart = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 315 Calendar calEnd = Calendar.getInstance(calStart.getTimeZone()); 316 calStart.setTimeInMillis(startDate); 317 calEnd.setTimeInMillis(endDate); 318 319 // get the required info to build the date range from the calendars 320 int startDay = calStart.get(Calendar.DAY_OF_MONTH); 321 int endDay = calEnd.get(Calendar.DAY_OF_MONTH); 322 int maxDayInStartMonth = calStart.getActualMaximum(Calendar.DAY_OF_MONTH); 323 int startMonth = calStart.get(Calendar.MONTH) + 1; 324 int endMonth = calEnd.get(Calendar.MONTH) + 1; 325 int startYear = calStart.get(Calendar.YEAR); 326 int endYear = calEnd.get(Calendar.YEAR); 327 328 // first add all full years in the date range 329 result.addAll(getYearSpan(startYear + 1, endYear - 1)); 330 331 if (startYear != endYear) { 332 // different year, different month 333 result.addAll(getMonthSpan(startMonth + 1, 12, startYear)); 334 result.addAll(getMonthSpan(1, endMonth - 1, endYear)); 335 result.addAll(getDaySpan(startDay, maxDayInStartMonth, startMonth, startYear)); 336 result.addAll(getDaySpan(1, endDay, endMonth, endYear)); 337 } else { 338 if (startMonth != endMonth) { 339 // same year, different month 340 result.addAll(getMonthSpan(startMonth + 1, endMonth - 1, startYear)); 341 result.addAll(getDaySpan(startDay, maxDayInStartMonth, startMonth, startYear)); 342 result.addAll(getDaySpan(1, endDay, endMonth, endYear)); 343 } else { 344 // same year, same month 345 result.addAll(getDaySpan(startDay, endDay, endMonth, endYear)); 346 } 347 } 348 349 // sort the result, makes the range better readable in the debugger 350 Collections.sort(result); 351 return result; 352 } 353 354 /** 355 * Calculate a span of days in the given year and month for the optimized date range search.<p> 356 * 357 * The result will contain dates formatted like "yyyyMMDD", for example "20080131".<p> 358 * 359 * @param startDay the start day 360 * @param endDay the end day 361 * @param month the month 362 * @param year the year 363 * 364 * @return a span of days in the given year and month for the optimized date range search 365 */ 366 private static List<String> getDaySpan(int startDay, int endDay, int month, int year) { 367 368 List<String> result = new ArrayList<String>(); 369 String yearMonthStr = String.valueOf(year) + DATES[month]; 370 for (int i = startDay; i <= endDay; i++) { 371 String dateStr = yearMonthStr + DATES[i]; 372 result.add(dateStr); 373 } 374 return result; 375 } 376 377 /** 378 * Calculate a span of months in the given year for the optimized date range search.<p> 379 * 380 * The result will contain dates formatted like "yyyyMM", for example "200801".<p> 381 * 382 * @param startMonth the start month 383 * @param endMonth the end month 384 * @param year the year 385 * 386 * @return a span of months in the given year for the optimized date range search 387 */ 388 private static List<String> getMonthSpan(int startMonth, int endMonth, int year) { 389 390 List<String> result = new ArrayList<String>(); 391 String yearStr = String.valueOf(year); 392 for (int i = startMonth; i <= endMonth; i++) { 393 String dateStr = yearStr + DATES[i]; 394 result.add(dateStr); 395 } 396 return result; 397 } 398 399 /** 400 * Calculate a span of years for the optimized date range search.<p> 401 * 402 * The result will contain dates formatted like "yyyy", for example "2008".<p> 403 * 404 * @param startYear the start year 405 * @param endYear the end year 406 * 407 * @return a span of years for the optimized date range search 408 */ 409 private static List<String> getYearSpan(int startYear, int endYear) { 410 411 List<String> result = new ArrayList<String>(); 412 for (int i = startYear; i <= endYear; i++) { 413 String dateStr = String.valueOf(i); 414 result.add(dateStr); 415 } 416 return result; 417 } 418 419 /** 420 * Adds a parameter.<p> 421 * 422 * @param key the key/name of the parameter 423 * @param value the value of the parameter 424 * 425 */ 426 @Override 427 public void addConfigurationParameter(String key, String value) { 428 429 if (PERMISSIONS.equals(key)) { 430 m_checkPermissions = Boolean.valueOf(value).booleanValue(); 431 } else if (EXTRACT_CONTENT.equals(key)) { 432 setExtractContent(Boolean.valueOf(value).booleanValue()); 433 } else if (BACKUP_REINDEXING.equals(key)) { 434 m_backupReindexing = Boolean.valueOf(value).booleanValue(); 435 } else if (LANGUAGEDETECTION.equals(key)) { 436 setLanguageDetection(Boolean.valueOf(value).booleanValue()); 437 } else if (IGNORE_EXPIRATION.equals(key)) { 438 m_ignoreExpiration = Boolean.valueOf(value).booleanValue(); 439 } else if (PRIORITY.equals(key)) { 440 m_priority = Integer.parseInt(value); 441 if (m_priority < Thread.MIN_PRIORITY) { 442 m_priority = Thread.MIN_PRIORITY; 443 LOG.error( 444 Messages.get().getBundle().key( 445 Messages.LOG_SEARCH_PRIORITY_TOO_LOW_2, 446 value, 447 Integer.valueOf(Thread.MIN_PRIORITY))); 448 449 } else if (m_priority > Thread.MAX_PRIORITY) { 450 m_priority = Thread.MAX_PRIORITY; 451 LOG.debug( 452 Messages.get().getBundle().key( 453 Messages.LOG_SEARCH_PRIORITY_TOO_HIGH_2, 454 value, 455 Integer.valueOf(Thread.MAX_PRIORITY))); 456 } 457 } 458 459 if (MAX_HITS.equals(key)) { 460 try { 461 m_maxHits = Integer.parseInt(value); 462 } catch (NumberFormatException e) { 463 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 464 } 465 if (m_maxHits < (MAX_HITS_DEFAULT / 100)) { 466 m_maxHits = MAX_HITS_DEFAULT; 467 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 468 } 469 } else if (TIME_RANGE.equals(key)) { 470 m_checkTimeRange = Boolean.valueOf(value).booleanValue(); 471 } else if (CmsSearchIndex.EXCERPT.equals(key)) { 472 m_createExcerpt = Boolean.valueOf(value).booleanValue(); 473 474 } else if (LUCENE_RAM_BUFFER_SIZE_MB.equals(key)) { 475 try { 476 m_luceneRAMBufferSizeMB = Double.valueOf(value); 477 } catch (NumberFormatException e) { 478 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 479 } 480 } 481 } 482 483 /** 484 * Creates an empty document that can be used by this search field configuration.<p> 485 * 486 * @param resource the resource to create the document for 487 * 488 * @return a new and empty document 489 */ 490 public I_CmsSearchDocument createEmptyDocument(CmsResource resource) { 491 492 return new CmsLuceneDocument(new Document()); 493 } 494 495 /** 496 * Returns the Lucene analyzer used for this index.<p> 497 * 498 * @return the Lucene analyzer used for this index 499 */ 500 public Analyzer getAnalyzer() { 501 502 return m_analyzer; 503 } 504 505 /** 506 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 507 */ 508 @Override 509 public CmsParameterConfiguration getConfiguration() { 510 511 CmsParameterConfiguration result = new CmsParameterConfiguration(); 512 if (getPriority() > 0) { 513 result.put(PRIORITY, String.valueOf(m_priority)); 514 } 515 if (!isExtractingContent()) { 516 result.put(EXTRACT_CONTENT, String.valueOf(isExtractingContent())); 517 } 518 if (!isCheckingPermissions()) { 519 result.put(PERMISSIONS, String.valueOf(m_checkPermissions)); 520 } 521 if (isBackupReindexing()) { 522 result.put(BACKUP_REINDEXING, String.valueOf(m_backupReindexing)); 523 } 524 if (isLanguageDetection()) { 525 result.put(LANGUAGEDETECTION, String.valueOf(isLanguageDetection())); 526 } 527 if (getMaxHits() != MAX_HITS_DEFAULT) { 528 result.put(MAX_HITS, String.valueOf(getMaxHits())); 529 } 530 if (!isCreatingExcerpt()) { 531 result.put(EXCERPT, String.valueOf(m_createExcerpt)); 532 } 533 if (m_luceneRAMBufferSizeMB != null) { 534 result.put(LUCENE_RAM_BUFFER_SIZE_MB, String.valueOf(m_luceneRAMBufferSizeMB)); 535 } 536 // always write time range check parameter because of logic change in OpenCms 8.0 537 result.put(TIME_RANGE, String.valueOf(m_checkTimeRange)); 538 return result; 539 } 540 541 /** 542 * @see org.opencms.search.I_CmsSearchIndex#getContentIfUnchanged(org.opencms.file.CmsResource) 543 */ 544 @Override 545 public I_CmsExtractionResult getContentIfUnchanged(CmsResource resource) { 546 547 // compare "date of last modification of content" from Lucene index and OpenCms VFS 548 // if this is identical, then the data from the Lucene index can be re-used 549 I_CmsSearchDocument oldDoc = getDocument(CmsSearchField.FIELD_PATH, resource.getRootPath()); 550 // first check if the document is already in the index 551 if ((oldDoc != null) && (oldDoc.getFieldValueAsDate(CmsSearchField.FIELD_DATE_CONTENT) != null)) { 552 long contentDateIndex = oldDoc.getFieldValueAsDate(CmsSearchField.FIELD_DATE_CONTENT).getTime(); 553 // now compare the date with the date stored in the resource 554 // we truncate to seconds, since the index stores no milliseconds 555 // and it seems practically irrelevant that a content is updated twice in a second. 556 if ((contentDateIndex / 1000L) == (resource.getDateContent() / 1000L)) { 557 // extract stored content blob from index 558 return CmsExtractionResult.fromBytes(oldDoc.getContentBlob()); 559 } 560 } 561 return null; 562 } 563 564 /** 565 * Returns a document by document ID.<p> 566 * 567 * @param docId the id to get the document for 568 * 569 * @return the CMS specific document 570 */ 571 public I_CmsSearchDocument getDocument(int docId) { 572 573 try { 574 IndexSearcher searcher = getSearcher(); 575 return new CmsLuceneDocument(searcher.doc(docId)); 576 } catch (IOException e) { 577 // ignore, return null and assume document was not found 578 } 579 return null; 580 } 581 582 /** 583 * Returns the Lucene document with the given root path from the index.<p> 584 * 585 * @param rootPath the root path of the document to get 586 * 587 * @return the Lucene document with the given root path from the index 588 * 589 * @deprecated Use {@link #getDocument(String, String)} instead and provide {@link org.opencms.search.fields.CmsLuceneField#FIELD_PATH} as field to search in 590 */ 591 @Deprecated 592 public Document getDocument(String rootPath) { 593 594 if (getDocument(CmsSearchField.FIELD_PATH, rootPath) != null) { 595 return (Document)getDocument(CmsSearchField.FIELD_PATH, rootPath).getDocument(); 596 } 597 return null; 598 } 599 600 /** 601 * Returns the first document where the given term matches the selected index field.<p> 602 * 603 * Use this method to search for documents which have unique field values, like a unique id.<p> 604 * 605 * @param field the field to search in 606 * @param term the term to search for 607 * 608 * @return the first document where the given term matches the selected index field 609 */ 610 public I_CmsSearchDocument getDocument(String field, String term) { 611 612 Document result = null; 613 IndexSearcher searcher = getSearcher(); 614 if (searcher != null) { 615 // search for an exact match on the selected field 616 Term resultTerm = new Term(field, term); 617 try { 618 TopDocs hits = searcher.search(new TermQuery(resultTerm), 1); 619 if (hits.scoreDocs.length > 0) { 620 result = searcher.doc(hits.scoreDocs[0].doc); 621 } 622 } catch (IOException e) { 623 // ignore, return null and assume document was not found 624 } 625 } 626 if (result != null) { 627 return new CmsLuceneDocument(result); 628 } 629 return null; 630 } 631 632 /** 633 * Returns the language locale for the given resource in this index.<p> 634 * 635 * @param cms the current OpenCms user context 636 * @param resource the resource to check 637 * @param availableLocales a list of locales supported by the resource 638 * 639 * @return the language locale for the given resource in this index 640 */ 641 @Override 642 public Locale getLocaleForResource(CmsObject cms, CmsResource resource, List<Locale> availableLocales) { 643 644 Locale result; 645 List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource); 646 List<Locale> locales = availableLocales; 647 if ((locales == null) || (locales.size() == 0)) { 648 locales = defaultLocales; 649 } 650 result = OpenCms.getLocaleManager().getBestMatchingLocale(getLocale(), defaultLocales, locales); 651 return result; 652 } 653 654 /** 655 * Returns the language locale of the index as a String.<p> 656 * 657 * @return the language locale of the index as a String 658 * 659 * @see #getLocale() 660 */ 661 public String getLocaleString() { 662 663 return getLocale().toString(); 664 } 665 666 /** 667 * Indicates the number of how many hits are loaded at maximum.<p> 668 * 669 * The number of maximum documents to load from the index 670 * must be specified. The default of this setting is {@link CmsSearchIndex#MAX_HITS_DEFAULT} (5000). 671 * This means that at maximum 5000 results are returned from the index. 672 * Please note that this number may be reduced further because of OpenCms read permissions 673 * or per-user file visibility settings not controlled in the index.<p> 674 * 675 * @return the number of how many hits are loaded at maximum 676 * 677 * @since 7.5.1 678 */ 679 public int getMaxHits() { 680 681 return m_maxHits; 682 } 683 684 /** 685 * Returns the path where this index stores it's data in the "real" file system.<p> 686 * 687 * @return the path where this index stores it's data in the "real" file system 688 */ 689 @Override 690 public String getPath() { 691 692 if (super.getPath() == null) { 693 setPath(generateIndexDirectory()); 694 } 695 return super.getPath(); 696 } 697 698 /** 699 * Returns the Thread priority for this search index.<p> 700 * 701 * @return the Thread priority for this search index 702 */ 703 public int getPriority() { 704 705 return m_priority; 706 } 707 708 /** 709 * Returns the Lucene index searcher used for this search index.<p> 710 * 711 * @return the Lucene index searcher used for this search index 712 */ 713 public IndexSearcher getSearcher() { 714 715 return m_indexSearcher; 716 } 717 718 /** 719 * @see org.opencms.search.A_CmsSearchIndex#initialize() 720 */ 721 @Override 722 public void initialize() throws CmsSearchException { 723 724 super.initialize(); 725 726 // get the configured analyzer and apply the the field configuration analyzer wrapper 727 @SuppressWarnings("resource") 728 Analyzer baseAnalyzer = OpenCms.getSearchManager().getAnalyzer(getLocale()); 729 730 if (getFieldConfiguration() instanceof CmsLuceneFieldConfiguration) { 731 CmsLuceneFieldConfiguration fc = (CmsLuceneFieldConfiguration)getFieldConfiguration(); 732 setAnalyzer(fc.getAnalyzer(baseAnalyzer)); 733 } 734 } 735 736 /** 737 * Returns <code>true</code> if backup re-indexing is done by this index.<p> 738 * 739 * This is an optimization method by which the old extracted content is 740 * reused in order to save performance when re-indexing.<p> 741 * 742 * @return <code>true</code> if backup re-indexing is done by this index 743 * 744 * @since 7.5.1 745 */ 746 public boolean isBackupReindexing() { 747 748 return m_backupReindexing; 749 } 750 751 /** 752 * Returns <code>true</code> if permissions are checked for search results by this index.<p> 753 * 754 * If permission checks are not required, they can be turned off in the index search configuration parameters 755 * in <code>opencms-search.xml</code>. Not checking permissions will improve performance.<p> 756 * 757 * This is can be of use in scenarios when you know that all search results are always readable, 758 * which is usually true for public websites that do not have personalized accounts.<p> 759 * 760 * Please note that even if a result is returned where the current user has no read permissions, 761 * the user can not actually access this document. It will only appear in the search result list, 762 * but if the user clicks the link to open the document he will get an error.<p> 763 * 764 * 765 * @return <code>true</code> if permissions are checked for search results by this index 766 */ 767 public boolean isCheckingPermissions() { 768 769 return m_checkPermissions; 770 } 771 772 /** 773 * Returns <code>true</code> if the document time range is checked with a granularity level of seconds 774 * for search results by this index.<p> 775 * 776 * Since OpenCms 8.0, time range checks are always done if {@link CmsSearchParameters#setMinDateLastModified(long)} 777 * or any of the corresponding methods are used. 778 * This is done very efficiently using optimized Lucene filers. 779 * However, the granularity of these checks are done only on a daily 780 * basis, which means that you can only find "changes made yesterday" but not "changes made last hour". 781 * For normal limitation of search results, a daily granularity should be enough.<p> 782 * 783 * If time range checks with a granularity level of seconds are required, 784 * they can be turned on in the index search configuration parameters 785 * in <code>opencms-search.xml</code>. 786 * Not checking the time range with a granularity level of seconds will improve performance.<p> 787 * 788 * By default the granularity level of seconds is turned off since OpenCms 8.0<p> 789 * 790 * @return <code>true</code> if the document time range is checked with a granularity level of seconds for search results by this index 791 */ 792 public boolean isCheckingTimeRange() { 793 794 return m_checkTimeRange; 795 } 796 797 /** 798 * Returns the checkPermissions.<p> 799 * 800 * @return the checkPermissions 801 */ 802 public boolean isCheckPermissions() { 803 804 return m_checkPermissions; 805 } 806 807 /** 808 * Returns <code>true</code> if an excerpt is generated by this index.<p> 809 * 810 * If no except is required, generation can be turned off in the index search configuration parameters 811 * in <code>opencms-search.xml</code>. Not generating an excerpt will improve performance.<p> 812 * 813 * @return <code>true</code> if an excerpt is generated by this index 814 */ 815 public boolean isCreatingExcerpt() { 816 817 return m_createExcerpt; 818 } 819 820 /** 821 * Returns the ignoreExpiration.<p> 822 * 823 * @return the ignoreExpiration 824 */ 825 public boolean isIgnoreExpiration() { 826 827 return m_ignoreExpiration; 828 } 829 830 /** 831 * @see org.opencms.search.A_CmsSearchIndex#isInitialized() 832 */ 833 @Override 834 public boolean isInitialized() { 835 836 return super.isInitialized() && (null != getPath()); 837 } 838 839 /** 840 * Returns <code>true</code> if a resource requires read permission to be included in the result list.<p> 841 * 842 * @return <code>true</code> if a resource requires read permission to be included in the result list 843 */ 844 public boolean isRequireViewPermission() { 845 846 return m_requireViewPermission; 847 } 848 849 /** 850 * @see org.opencms.search.A_CmsSearchIndex#onIndexChanged(boolean) 851 */ 852 @Override 853 public void onIndexChanged(boolean force) { 854 855 if (force) { 856 indexSearcherOpen(getPath()); 857 } else { 858 indexSearcherUpdate(); 859 } 860 } 861 862 /** 863 * Performs a search on the index within the given fields.<p> 864 * 865 * The result is returned as List with entries of type I_CmsSearchResult.<p> 866 * 867 * @param cms the current user's Cms object 868 * @param params the parameters to use for the search 869 * 870 * @return the List of results found or an empty list 871 * 872 * @throws CmsSearchException if something goes wrong 873 */ 874 public CmsSearchResultList search(CmsObject cms, CmsSearchParameters params) throws CmsSearchException { 875 876 long timeTotal = -System.currentTimeMillis(); 877 long timeLucene; 878 long timeResultProcessing; 879 880 if (LOG.isDebugEnabled()) { 881 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SEARCH_PARAMS_2, params, getName())); 882 } 883 884 // the hits found during the search 885 TopDocs hits; 886 887 // storage for the results found 888 CmsSearchResultList searchResults = new CmsSearchResultList(); 889 890 int previousPriority = Thread.currentThread().getPriority(); 891 892 try { 893 // copy the user OpenCms context 894 CmsObject searchCms = OpenCms.initCmsObject(cms); 895 896 if (getPriority() > 0) { 897 // change thread priority in order to reduce search impact on overall system performance 898 Thread.currentThread().setPriority(getPriority()); 899 } 900 901 // change the project 902 searchCms.getRequestContext().setCurrentProject(searchCms.readProject(getProject())); 903 904 timeLucene = -System.currentTimeMillis(); 905 906 // several search options are searched using filters 907 BooleanQuery.Builder builder = new BooleanQuery.Builder(); 908 // append root path filter 909 builder = appendPathFilter(searchCms, builder, params.getRoots()); 910 // append category filter 911 builder = appendCategoryFilter(searchCms, builder, params.getCategories()); 912 // append resource type filter 913 builder = appendResourceTypeFilter(searchCms, builder, params.getResourceTypes()); 914 915 // append date last modified filter 916 builder = appendDateLastModifiedFilter( 917 builder, 918 params.getMinDateLastModified(), 919 params.getMaxDateLastModified()); 920 // append date created filter 921 builder = appendDateCreatedFilter(builder, params.getMinDateCreated(), params.getMaxDateCreated()); 922 923 // the search query to use, will be constructed in the next lines 924 Query query = null; 925 // store separate fields query for excerpt highlighting 926 Query fieldsQuery = null; 927 928 // get an index searcher that is certainly up to date 929 indexSearcherUpdate(); 930 IndexSearcher searcher = getSearcher(); 931 932 if (!params.isIgnoreQuery()) { 933 // since OpenCms 8 the query can be empty in which case only filters are used for the result 934 if (params.getParsedQuery() != null) { 935 // the query was already build, re-use it 936 QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer()); 937 fieldsQuery = p.parse(params.getParsedQuery()); 938 } else if (params.getFieldQueries() != null) { 939 // each field has an individual query 940 BooleanQuery.Builder mustOccur = null; 941 BooleanQuery.Builder shouldOccur = null; 942 for (CmsSearchParameters.CmsSearchFieldQuery fq : params.getFieldQueries()) { 943 // add one sub-query for each defined field 944 QueryParser p = new QueryParser(fq.getFieldName(), getAnalyzer()); 945 // first generate the combined keyword query 946 Query keywordQuery = null; 947 if (fq.getSearchTerms().size() == 1) { 948 // this is just a single size keyword list 949 keywordQuery = p.parse(fq.getSearchTerms().get(0)); 950 } else { 951 // multiple size keyword list 952 BooleanQuery.Builder keywordListQuery = new BooleanQuery.Builder(); 953 for (String keyword : fq.getSearchTerms()) { 954 keywordListQuery.add(p.parse(keyword), fq.getTermOccur()); 955 } 956 keywordQuery = keywordListQuery.build(); 957 } 958 if (BooleanClause.Occur.SHOULD.equals(fq.getOccur())) { 959 if (shouldOccur == null) { 960 shouldOccur = new BooleanQuery.Builder(); 961 } 962 shouldOccur.add(keywordQuery, fq.getOccur()); 963 } else { 964 if (mustOccur == null) { 965 mustOccur = new BooleanQuery.Builder(); 966 } 967 mustOccur.add(keywordQuery, fq.getOccur()); 968 } 969 } 970 BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder(); 971 if (mustOccur != null) { 972 booleanFieldsQuery.add(mustOccur.build(), BooleanClause.Occur.MUST); 973 } 974 if (shouldOccur != null) { 975 booleanFieldsQuery.add(shouldOccur.build(), BooleanClause.Occur.MUST); 976 } 977 fieldsQuery = searcher.rewrite(booleanFieldsQuery.build()); 978 } else if ((params.getFields() != null) && (params.getFields().size() > 0)) { 979 // no individual field queries have been defined, so use one query for all fields 980 BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder(); 981 // this is a "regular" query over one or more fields 982 // add one sub-query for each of the selected fields, e.g. "content", "title" etc. 983 for (int i = 0; i < params.getFields().size(); i++) { 984 QueryParser p = new QueryParser(params.getFields().get(i), getAnalyzer()); 985 p.setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_REWRITE); 986 booleanFieldsQuery.add(p.parse(params.getQuery()), BooleanClause.Occur.SHOULD); 987 } 988 fieldsQuery = searcher.rewrite(booleanFieldsQuery.build()); 989 } else { 990 // if no fields are provided, just use the "content" field by default 991 QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer()); 992 fieldsQuery = searcher.rewrite(p.parse(params.getQuery())); 993 } 994 995 // finally set the main query to the fields query 996 // please note that we still need both variables in case the query is a MatchAllDocsQuery - see below 997 query = fieldsQuery; 998 } 999 1000 if (LOG.isDebugEnabled()) { 1001 LOG.debug(Messages.get().getBundle().key(Messages.LOG_BASE_QUERY_1, query)); 1002 } 1003 1004 if (query == null) { 1005 // if no text query is set, then we match all documents 1006 query = new MatchAllDocsQuery(); 1007 } else { 1008 // store the parsed query for page browsing 1009 params.setParsedQuery(query.toString(CmsSearchField.FIELD_CONTENT)); 1010 } 1011 1012 // build the final query 1013 final BooleanQuery.Builder finalQueryBuilder = new BooleanQuery.Builder(); 1014 finalQueryBuilder.add(query, BooleanClause.Occur.MUST); 1015 finalQueryBuilder.add(builder.build(), BooleanClause.Occur.FILTER); 1016 final BooleanQuery finalQuery = finalQueryBuilder.build(); 1017 1018 // collect the categories 1019 CmsSearchCategoryCollector categoryCollector; 1020 if (params.isCalculateCategories()) { 1021 // USE THIS OPTION WITH CAUTION 1022 // this may slow down searched by an order of magnitude 1023 categoryCollector = new CmsSearchCategoryCollector(searcher); 1024 // perform a first search to collect the categories 1025 searcher.search(finalQuery, categoryCollector); 1026 // store the result 1027 searchResults.setCategories(categoryCollector.getCategoryCountResult()); 1028 } 1029 1030 // get maxScore first, since Lucene 8, it's not computed automatically anymore 1031 TopDocs scoreHits = searcher.search(query, 1); 1032 float maxScore = scoreHits.scoreDocs.length == 0 ? Float.NaN : scoreHits.scoreDocs[0].score; 1033 // perform the search operation 1034 if ((params.getSort() == null) || (params.getSort() == CmsSearchParameters.SORT_DEFAULT)) { 1035 // apparently scoring is always enabled by Lucene if no sort order is provided 1036 hits = searcher.search(finalQuery, getMaxHits()); 1037 } else { 1038 // if a sort order is provided, we must check if scoring must be calculated by the searcher 1039 boolean isSortScore = isSortScoring(searcher, params.getSort()); 1040 hits = searcher.search(finalQuery, getMaxHits(), params.getSort(), isSortScore); 1041 } 1042 1043 timeLucene += System.currentTimeMillis(); 1044 timeResultProcessing = -System.currentTimeMillis(); 1045 1046 if (hits != null) { 1047 long hitCount = hits.totalHits.value > hits.scoreDocs.length 1048 ? hits.scoreDocs.length 1049 : hits.totalHits.value; 1050 int page = params.getSearchPage(); 1051 long start = -1, end = -1; 1052 if ((params.getMatchesPerPage() > 0) && (page > 0) && (hitCount > 0)) { 1053 // calculate the final size of the search result 1054 start = params.getMatchesPerPage() * (page - 1); 1055 end = start + params.getMatchesPerPage(); 1056 // ensure that both i and n are inside the range of foundDocuments.size() 1057 start = (start > hitCount) ? hitCount : start; 1058 end = (end > hitCount) ? hitCount : end; 1059 } else { 1060 // return all found documents in the search result 1061 start = 0; 1062 end = hitCount; 1063 } 1064 1065 Set<String> returnFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getReturnFields(); 1066 Set<String> excerptFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getExcerptFields(); 1067 1068 long visibleHitCount = hitCount; 1069 for (int i = 0, cnt = 0; (i < hitCount) && (cnt < end); i++) { 1070 try { 1071 Document doc = searcher.doc(hits.scoreDocs[i].doc, returnFields); 1072 I_CmsSearchDocument searchDoc = new CmsLuceneDocument(doc); 1073 searchDoc.setScore(hits.scoreDocs[i].score); 1074 if ((isInTimeRange(doc, params)) && (hasReadPermission(searchCms, searchDoc))) { 1075 // user has read permission 1076 if (cnt >= start) { 1077 // do not use the resource to obtain the raw content, read it from the lucene document! 1078 String excerpt = null; 1079 if (isCreatingExcerpt() && (fieldsQuery != null)) { 1080 Document exDoc = searcher.doc(hits.scoreDocs[i].doc, excerptFields); 1081 I_CmsTermHighlighter highlighter = OpenCms.getSearchManager().getHighlighter(); 1082 excerpt = highlighter.getExcerpt(exDoc, this, params, fieldsQuery, getAnalyzer()); 1083 } 1084 int score = Math.round( 1085 (maxScore != Float.NaN ? (hits.scoreDocs[i].score / maxScore) * 100f : 0)); 1086 searchResults.add(new CmsSearchResult(score, doc, excerpt)); 1087 } 1088 cnt++; 1089 } else { 1090 visibleHitCount--; 1091 } 1092 } catch (Exception e) { 1093 // should not happen, but if it does we want to go on with the next result nevertheless 1094 if (LOG.isWarnEnabled()) { 1095 LOG.warn(Messages.get().getBundle().key(Messages.LOG_RESULT_ITERATION_FAILED_0), e); 1096 } 1097 } 1098 } 1099 1100 // save the total count of search results 1101 searchResults.setHitCount((int)visibleHitCount); 1102 } else { 1103 searchResults.setHitCount(0); 1104 } 1105 1106 timeResultProcessing += System.currentTimeMillis(); 1107 } catch (RuntimeException e) { 1108 throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e); 1109 } catch (Exception e) { 1110 throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e); 1111 } finally { 1112 1113 // re-set thread to previous priority 1114 Thread.currentThread().setPriority(previousPriority); 1115 } 1116 1117 if (LOG.isDebugEnabled()) { 1118 timeTotal += System.currentTimeMillis(); 1119 Object[] logParams = new Object[] { 1120 Long.valueOf(hits == null ? 0 : hits.totalHits.value), 1121 Long.valueOf(timeTotal), 1122 Long.valueOf(timeLucene), 1123 Long.valueOf(timeResultProcessing)}; 1124 LOG.debug(Messages.get().getBundle().key(Messages.LOG_STAT_RESULTS_TIME_4, logParams)); 1125 } 1126 1127 return searchResults; 1128 } 1129 1130 /** 1131 * Sets the Lucene analyzer used for this index.<p> 1132 * 1133 * @param analyzer the Lucene analyzer to set 1134 */ 1135 public void setAnalyzer(Analyzer analyzer) { 1136 1137 m_analyzer = analyzer; 1138 } 1139 1140 /** 1141 * Sets the checkPermissions.<p> 1142 * 1143 * @param checkPermissions the checkPermissions to set 1144 */ 1145 public void setCheckPermissions(boolean checkPermissions) { 1146 1147 m_checkPermissions = checkPermissions; 1148 } 1149 1150 /** 1151 * Sets the ignoreExpiration.<p> 1152 * 1153 * @param ignoreExpiration the ignoreExpiration to set 1154 */ 1155 public void setIgnoreExpiration(boolean ignoreExpiration) { 1156 1157 m_ignoreExpiration = ignoreExpiration; 1158 } 1159 1160 /** 1161 * Sets the number of how many hits are loaded at maximum.<p> 1162 * 1163 * This must be set at least to 50, or this setting is ignored.<p> 1164 * 1165 * @param maxHits the number of how many hits are loaded at maximum to set 1166 * 1167 * @see #getMaxHits() 1168 * 1169 * @since 7.5.1 1170 */ 1171 public void setMaxHits(int maxHits) { 1172 1173 if (m_maxHits >= (MAX_HITS_DEFAULT / 100)) { 1174 m_maxHits = maxHits; 1175 } 1176 } 1177 1178 /** 1179 * Controls if a resource requires view permission to be displayed in the result list.<p> 1180 * 1181 * By default this is <code>false</code>.<p> 1182 * 1183 * @param requireViewPermission controls if a resource requires view permission to be displayed in the result list 1184 */ 1185 public void setRequireViewPermission(boolean requireViewPermission) { 1186 1187 m_requireViewPermission = requireViewPermission; 1188 } 1189 1190 /** 1191 * Shuts down the search index.<p> 1192 * 1193 * This will close the local Lucene index searcher instance.<p> 1194 */ 1195 @Override 1196 public void shutDown() { 1197 1198 super.shutDown(); 1199 indexSearcherClose(); 1200 if (m_analyzer != null) { 1201 m_analyzer.close(); 1202 } 1203 if (CmsLog.INIT.isInfoEnabled()) { 1204 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_INDEX_1, getName())); 1205 } 1206 } 1207 1208 /** 1209 * Returns the name (<code>{@link #getName()}</code>) of this search index.<p> 1210 * 1211 * @return the name (<code>{@link #getName()}</code>) of this search index 1212 * 1213 * @see java.lang.Object#toString() 1214 */ 1215 @Override 1216 public String toString() { 1217 1218 return getName(); 1219 } 1220 1221 /** 1222 * Appends the a category filter to the given filter clause that matches all given categories.<p> 1223 * 1224 * In case the provided List is null or empty, the original filter is left unchanged.<p> 1225 * 1226 * The original filter parameter is extended and also provided as return value.<p> 1227 * 1228 * @param cms the current OpenCms search context 1229 * @param filter the filter to extend 1230 * @param categories the categories that will compose the filter 1231 * 1232 * @return the extended filter clause 1233 */ 1234 protected BooleanQuery.Builder appendCategoryFilter( 1235 CmsObject cms, 1236 BooleanQuery.Builder filter, 1237 List<String> categories) { 1238 1239 if ((categories != null) && (categories.size() > 0)) { 1240 // add query categories (if required) 1241 1242 // categories are indexed as lower-case strings 1243 // @see org.opencms.search.fields.CmsSearchFieldConfiguration#appendCategories 1244 List<String> lowerCaseCategories = new ArrayList<String>(); 1245 for (String category : categories) { 1246 lowerCaseCategories.add(category.toLowerCase()); 1247 } 1248 filter.add( 1249 new BooleanClause( 1250 getMultiTermQueryFilter(CmsSearchField.FIELD_CATEGORY, lowerCaseCategories), 1251 BooleanClause.Occur.MUST)); 1252 } 1253 1254 return filter; 1255 } 1256 1257 /** 1258 * Appends a date of creation filter to the given filter clause that matches the 1259 * given time range.<p> 1260 * 1261 * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE} 1262 * than the original filter is left unchanged.<p> 1263 * 1264 * The original filter parameter is extended and also provided as return value.<p> 1265 * 1266 * @param filter the filter to extend 1267 * @param startTime start time of the range to search in 1268 * @param endTime end time of the range to search in 1269 * 1270 * @return the extended filter clause 1271 */ 1272 protected BooleanQuery.Builder appendDateCreatedFilter(BooleanQuery.Builder filter, long startTime, long endTime) { 1273 1274 // create special optimized sub-filter for the date last modified search 1275 Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_CREATED_LOOKUP, startTime, endTime); 1276 if (dateFilter != null) { 1277 // extend main filter with the created date filter 1278 filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST)); 1279 } 1280 1281 return filter; 1282 } 1283 1284 /** 1285 * Appends a date of last modification filter to the given filter clause that matches the 1286 * given time range.<p> 1287 * 1288 * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE} 1289 * than the original filter is left unchanged.<p> 1290 * 1291 * The original filter parameter is extended and also provided as return value.<p> 1292 * 1293 * @param filter the filter to extend 1294 * @param startTime start time of the range to search in 1295 * @param endTime end time of the range to search in 1296 * 1297 * @return the extended filter clause 1298 */ 1299 protected BooleanQuery.Builder appendDateLastModifiedFilter( 1300 BooleanQuery.Builder filter, 1301 long startTime, 1302 long endTime) { 1303 1304 // create special optimized sub-filter for the date last modified search 1305 Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_LASTMODIFIED_LOOKUP, startTime, endTime); 1306 if (dateFilter != null) { 1307 // extend main filter with the created date filter 1308 filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST)); 1309 } 1310 1311 return filter; 1312 } 1313 1314 /** 1315 * Appends the a VFS path filter to the given filter clause that matches all given root paths.<p> 1316 * 1317 * In case the provided List is null or empty, the current request context site root is appended.<p> 1318 * 1319 * The original filter parameter is extended and also provided as return value.<p> 1320 * 1321 * @param cms the current OpenCms search context 1322 * @param filter the filter to extend 1323 * @param roots the VFS root paths that will compose the filter 1324 * 1325 * @return the extended filter clause 1326 */ 1327 protected BooleanQuery.Builder appendPathFilter(CmsObject cms, BooleanQuery.Builder filter, List<String> roots) { 1328 1329 // complete the search root 1330 List<Term> terms = new ArrayList<Term>(); 1331 if ((roots != null) && (roots.size() > 0)) { 1332 // add the all configured search roots with will request context 1333 for (int i = 0; i < roots.size(); i++) { 1334 String searchRoot = cms.getRequestContext().addSiteRoot(roots.get(i)); 1335 extendPathFilter(terms, searchRoot); 1336 } 1337 } else { 1338 // use the current site root as the search root 1339 extendPathFilter(terms, cms.getRequestContext().getSiteRoot()); 1340 // also add the shared folder (v 8.0) 1341 if (OpenCms.getSiteManager().getSharedFolder() != null) { 1342 extendPathFilter(terms, OpenCms.getSiteManager().getSharedFolder()); 1343 } 1344 } 1345 1346 // add the calculated path filter for the root path 1347 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1348 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1349 filter.add(new BooleanClause(build.build(), BooleanClause.Occur.MUST)); 1350 return filter; 1351 } 1352 1353 /** 1354 * Appends the a resource type filter to the given filter clause that matches all given resource types.<p> 1355 * 1356 * In case the provided List is null or empty, the original filter is left unchanged.<p> 1357 * 1358 * The original filter parameter is extended and also provided as return value.<p> 1359 * 1360 * @param cms the current OpenCms search context 1361 * @param filter the filter to extend 1362 * @param resourceTypes the resource types that will compose the filter 1363 * 1364 * @return the extended filter clause 1365 */ 1366 protected BooleanQuery.Builder appendResourceTypeFilter( 1367 CmsObject cms, 1368 BooleanQuery.Builder filter, 1369 List<String> resourceTypes) { 1370 1371 if ((resourceTypes != null) && (resourceTypes.size() > 0)) { 1372 // add query resource types (if required) 1373 filter.add( 1374 new BooleanClause( 1375 getMultiTermQueryFilter(CmsSearchField.FIELD_TYPE, resourceTypes), 1376 BooleanClause.Occur.MUST)); 1377 } 1378 1379 return filter; 1380 } 1381 1382 /** 1383 * Creates an optimized date range filter for the date of last modification or creation.<p> 1384 * 1385 * If the start date is equal to {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE} 1386 * than <code>null</code> is returned.<p> 1387 * 1388 * @param fieldName the name of the field to search 1389 * @param startTime start time of the range to search in 1390 * @param endTime end time of the range to search in 1391 * 1392 * @return an optimized date range filter for the date of last modification or creation 1393 */ 1394 protected Query createDateRangeFilter(String fieldName, long startTime, long endTime) { 1395 1396 Query filter = null; 1397 if ((startTime != Long.MIN_VALUE) || (endTime != Long.MAX_VALUE)) { 1398 // a date range has been set for this document search 1399 if (startTime == Long.MIN_VALUE) { 1400 // default start will always be "yyyy1231" in order to reduce term size 1401 Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 1402 cal.setTimeInMillis(endTime); 1403 cal.set(cal.get(Calendar.YEAR) - MAX_YEAR_RANGE, 11, 31, 0, 0, 0); 1404 startTime = cal.getTimeInMillis(); 1405 } else if (endTime == Long.MAX_VALUE) { 1406 // default end will always be "yyyy0101" in order to reduce term size 1407 Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 1408 cal.setTimeInMillis(startTime); 1409 cal.set(cal.get(Calendar.YEAR) + MAX_YEAR_RANGE, 0, 1, 0, 0, 0); 1410 endTime = cal.getTimeInMillis(); 1411 } 1412 1413 // get the list of all possible date range options 1414 List<String> dateRange = getDateRangeSpan(startTime, endTime); 1415 List<Term> terms = new ArrayList<Term>(); 1416 for (String range : dateRange) { 1417 terms.add(new Term(fieldName, range)); 1418 } 1419 // create the filter for the date 1420 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1421 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1422 filter = build.build(); 1423 } 1424 return filter; 1425 } 1426 1427 /** 1428 * Creates a backup of this index for optimized re-indexing of the whole content.<p> 1429 * 1430 * @return the path to the backup folder, or <code>null</code> in case no backup was created 1431 */ 1432 protected String createIndexBackup() { 1433 1434 if (!isBackupReindexing()) { 1435 // if no backup is generated we don't need to do anything 1436 return null; 1437 } 1438 1439 // check if the target directory already exists 1440 File file = new File(getPath()); 1441 if (!file.exists()) { 1442 // index does not exist yet, so we can't backup it 1443 return null; 1444 } 1445 String backupPath = getPath() + "_backup"; 1446 FSDirectory oldDir = null; 1447 FSDirectory newDir = null; 1448 try { 1449 // open file directory for Lucene 1450 oldDir = FSDirectory.open(file.toPath()); 1451 newDir = FSDirectory.open(Paths.get(backupPath)); 1452 for (String fileName : oldDir.listAll()) { 1453 newDir.copyFrom(oldDir, fileName, fileName, IOContext.DEFAULT); 1454 } 1455 } catch (Exception e) { 1456 LOG.error( 1457 Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_CREATE_3, getName(), getPath(), backupPath), 1458 e); 1459 backupPath = null; 1460 } finally { 1461 if (oldDir != null) { 1462 try { 1463 oldDir.close(); 1464 } catch (IOException e) { 1465 e.printStackTrace(); 1466 } 1467 } 1468 if (newDir != null) { 1469 try { 1470 newDir.close(); 1471 } catch (IOException e) { 1472 e.printStackTrace(); 1473 } 1474 } 1475 } 1476 return backupPath; 1477 } 1478 1479 /** 1480 * Creates a new index writer.<p> 1481 * 1482 * @param create if <code>true</code> a whole new index is created, if <code>false</code> an existing index is updated 1483 * @param report the report 1484 * 1485 * @return the created new index writer 1486 * 1487 * @throws CmsIndexException in case the writer could not be created 1488 * 1489 * @see #getIndexWriter(I_CmsReport, boolean) 1490 */ 1491 @Override 1492 protected I_CmsIndexWriter createIndexWriter(boolean create, I_CmsReport report) throws CmsIndexException { 1493 1494 IndexWriter indexWriter = null; 1495 FSDirectory dir = null; 1496 try { 1497 File f = new File(getPath()); 1498 if (!f.exists()) { 1499 f = f.getParentFile(); 1500 if ((f != null) && (!f.exists())) { 1501 f.mkdirs(); 1502 } 1503 1504 create = true; 1505 } 1506 1507 dir = FSDirectory.open(Paths.get(getPath())); 1508 IndexWriterConfig indexConfig = new IndexWriterConfig(getAnalyzer()); 1509 //indexConfig.setMergePolicy(mergePolicy); 1510 1511 if (m_luceneRAMBufferSizeMB != null) { 1512 indexConfig.setRAMBufferSizeMB(m_luceneRAMBufferSizeMB.doubleValue()); 1513 } 1514 if (create) { 1515 indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE); 1516 } else { 1517 indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); 1518 } 1519 // register the modified default similarity implementation 1520 indexConfig.setSimilarity(m_sim); 1521 1522 indexWriter = new IndexWriter(dir, indexConfig); 1523 } catch (Exception e) { 1524 if (dir != null) { 1525 try { 1526 dir.close(); 1527 } catch (IOException e1) { 1528 // TODO Auto-generated catch block 1529 e1.printStackTrace(); 1530 } 1531 } 1532 if (indexWriter != null) { 1533 try { 1534 indexWriter.close(); 1535 } catch (IOException closeExeception) { 1536 throw new CmsIndexException( 1537 Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()), 1538 e); 1539 } 1540 } 1541 throw new CmsIndexException( 1542 Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()), 1543 e); 1544 } 1545 1546 return new CmsLuceneIndexWriter(indexWriter, this); 1547 } 1548 1549 /** 1550 * Extends the given path query with another term for the given search root element.<p> 1551 * 1552 * @param terms the path filter to extend 1553 * @param searchRoot the search root to add to the path query 1554 */ 1555 protected void extendPathFilter(List<Term> terms, String searchRoot) { 1556 1557 if (!CmsResource.isFolder(searchRoot)) { 1558 searchRoot += "/"; 1559 } 1560 terms.add(new Term(CmsSearchField.FIELD_PARENT_FOLDERS, searchRoot)); 1561 } 1562 1563 /** 1564 * Generates the directory on the RFS for this index.<p> 1565 * 1566 * @return the directory on the RFS for this index 1567 */ 1568 protected String generateIndexDirectory() { 1569 1570 return OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf( 1571 OpenCms.getSearchManager().getDirectory() + "/" + getName()); 1572 } 1573 1574 /** 1575 * Returns a cached Lucene term query filter for the given field and terms.<p> 1576 * 1577 * @param field the field to use 1578 * @param terms the term to use 1579 * 1580 * @return a cached Lucene term query filter for the given field and terms 1581 */ 1582 protected Query getMultiTermQueryFilter(String field, List<String> terms) { 1583 1584 return getMultiTermQueryFilter(field, null, terms); 1585 } 1586 1587 /** 1588 * Returns a cached Lucene term query filter for the given field and terms.<p> 1589 * 1590 * @param field the field to use 1591 * @param terms the term to use 1592 * 1593 * @return a cached Lucene term query filter for the given field and terms 1594 */ 1595 protected Query getMultiTermQueryFilter(String field, String terms) { 1596 1597 return getMultiTermQueryFilter(field, terms, null); 1598 } 1599 1600 /** 1601 * Returns a cached Lucene term query filter for the given field and terms.<p> 1602 * 1603 * @param field the field to use 1604 * @param termsStr the terms to use as a String separated by a space ' ' char 1605 * @param termsList the list of terms to use 1606 * 1607 * @return a cached Lucene term query filter for the given field and terms 1608 */ 1609 protected Query getMultiTermQueryFilter(String field, String termsStr, List<String> termsList) { 1610 1611 if (termsStr == null) { 1612 StringBuffer buf = new StringBuffer(64); 1613 for (int i = 0; i < termsList.size(); i++) { 1614 if (i > 0) { 1615 buf.append(' '); 1616 } 1617 buf.append(termsList.get(i)); 1618 } 1619 termsStr = buf.toString(); 1620 } 1621 Query result = m_displayFilters.get( 1622 (new StringBuffer(64)).append(field).append('|').append(termsStr).toString()); 1623 if (result == null) { 1624 List<Term> terms = new ArrayList<Term>(); 1625 if (termsList == null) { 1626 termsList = CmsStringUtil.splitAsList(termsStr, ' '); 1627 } 1628 for (int i = 0; i < termsList.size(); i++) { 1629 terms.add(new Term(field, termsList.get(i))); 1630 } 1631 1632 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1633 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1634 Query termsQuery = build.build(); //termsFilter 1635 1636 try { 1637 result = termsQuery.createWeight(m_indexSearcher, ScoreMode.COMPLETE_NO_SCORES, 1).getQuery(); 1638 m_displayFilters.put(field + termsStr, result); 1639 } catch (IOException e) { 1640 // TODO don't know what happend 1641 e.printStackTrace(); 1642 } 1643 } 1644 return result; 1645 } 1646 1647 /** 1648 * Checks if the OpenCms resource referenced by the result document can be read 1649 * by the user of the given OpenCms context. 1650 * 1651 * Returns the referenced <code>CmsResource</code> or <code>null</code> if 1652 * the user is not permitted to read the resource.<p> 1653 * 1654 * @param cms the OpenCms user context to use for permission testing 1655 * @param doc the search result document to check 1656 * 1657 * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted 1658 */ 1659 protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc) { 1660 1661 // check if the resource exits in the VFS, 1662 // this will implicitly check read permission and if the resource was deleted 1663 CmsResourceFilter filter = CmsResourceFilter.DEFAULT; 1664 if (isRequireViewPermission()) { 1665 filter = CmsResourceFilter.DEFAULT_ONLY_VISIBLE; 1666 } else if (isIgnoreExpiration()) { 1667 filter = CmsResourceFilter.IGNORE_EXPIRATION; 1668 } 1669 1670 return getResource(cms, doc, filter); 1671 } 1672 1673 /** 1674 * Checks if the OpenCms resource referenced by the result document can be read 1675 * by the user of the given OpenCms context. 1676 * 1677 * Returns the referenced <code>CmsResource</code> or <code>null</code> if 1678 * the user is not permitted to read the resource.<p> 1679 * 1680 * @param cms the OpenCms user context to use for permission testing 1681 * @param doc the search result document to check 1682 * @param filter the resource filter to apply 1683 * 1684 * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted 1685 */ 1686 protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc, CmsResourceFilter filter) { 1687 1688 try { 1689 CmsObject clone = OpenCms.initCmsObject(cms); 1690 clone.getRequestContext().setSiteRoot(""); 1691 return clone.readResource(doc.getPath(), filter); 1692 } catch (CmsException e) { 1693 // Do nothing 1694 } 1695 1696 return null; 1697 } 1698 1699 /** 1700 * Returns a cached Lucene term query filter for the given field and term.<p> 1701 * 1702 * @param field the field to use 1703 * @param term the term to use 1704 * 1705 * @return a cached Lucene term query filter for the given field and term 1706 */ 1707 protected Query getTermQueryFilter(String field, String term) { 1708 1709 return getMultiTermQueryFilter(field, term, Collections.singletonList(term)); 1710 } 1711 1712 /** 1713 * Checks if the OpenCms resource referenced by the result document can be read 1714 * be the user of the given OpenCms context.<p> 1715 * 1716 * @param cms the OpenCms user context to use for permission testing 1717 * @param doc the search result document to check 1718 * @return <code>true</code> if the user has read permissions to the resource 1719 */ 1720 protected boolean hasReadPermission(CmsObject cms, I_CmsSearchDocument doc) { 1721 1722 // If no permission check is needed: the document can be read 1723 // Else try to read the resource if this is not possible the user does not have enough permissions 1724 return !needsPermissionCheck(doc) ? true : (null != getResource(cms, doc)); 1725 } 1726 1727 /** 1728 * Closes the index searcher for this index.<p> 1729 * 1730 * @see #indexSearcherOpen(String) 1731 */ 1732 protected synchronized void indexSearcherClose() { 1733 1734 indexSearcherClose(m_indexSearcher); 1735 } 1736 1737 /** 1738 * Closes the given Lucene index searcher.<p> 1739 * 1740 * @param searcher the searcher to close 1741 */ 1742 protected synchronized void indexSearcherClose(IndexSearcher searcher) { 1743 1744 // in case there is an index searcher available close it 1745 if ((searcher != null) && (searcher.getIndexReader() != null)) { 1746 try { 1747 searcher.getIndexReader().close(); 1748 } catch (Exception e) { 1749 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_CLOSE_1, getName()), e); 1750 } 1751 } 1752 } 1753 1754 /** 1755 * Initializes the index searcher for this index.<p> 1756 * 1757 * In case there is an index searcher still open, it is closed first.<p> 1758 * 1759 * For performance reasons, one instance of the index searcher should be kept 1760 * for all searches. However, if the index is updated or changed 1761 * this searcher instance needs to be re-initialized.<p> 1762 * 1763 * @param path the path to the index directory 1764 */ 1765 protected synchronized void indexSearcherOpen(String path) { 1766 1767 IndexSearcher oldSearcher = null; 1768 Directory indexDirectory = null; 1769 try { 1770 indexDirectory = FSDirectory.open(Paths.get(path)); 1771 if (DirectoryReader.indexExists(indexDirectory)) { 1772 IndexReader reader = UninvertingReader.wrap( 1773 DirectoryReader.open(indexDirectory), 1774 createUninvertingMap()); 1775 if (m_indexSearcher != null) { 1776 // store old searcher instance to close it later 1777 oldSearcher = m_indexSearcher; 1778 } 1779 m_indexSearcher = new IndexSearcher(reader); 1780 m_indexSearcher.setSimilarity(m_sim); 1781 m_displayFilters = new HashMap<>(); 1782 } 1783 } catch (IOException e) { 1784 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_1, getName()), e); 1785 if (indexDirectory != null) { 1786 try { 1787 indexDirectory.close(); 1788 } catch (IOException closeException) { 1789 // do nothing 1790 } 1791 } 1792 } 1793 if (oldSearcher != null) { 1794 // close the old searcher if required 1795 indexSearcherClose(oldSearcher); 1796 } 1797 } 1798 1799 /** 1800 * Reopens the index search reader for this index, required after the index has been changed.<p> 1801 * 1802 * @see #indexSearcherOpen(String) 1803 */ 1804 protected synchronized void indexSearcherUpdate() { 1805 1806 IndexSearcher oldSearcher = m_indexSearcher; 1807 if ((oldSearcher != null) && (oldSearcher.getIndexReader() != null)) { 1808 // in case there is an index searcher available close it 1809 try { 1810 if (oldSearcher.getIndexReader() instanceof DirectoryReader) { 1811 IndexReader newReader = DirectoryReader.openIfChanged( 1812 (DirectoryReader)oldSearcher.getIndexReader()); 1813 if (newReader != null) { 1814 m_indexSearcher = new IndexSearcher(newReader); 1815 m_indexSearcher.setSimilarity(m_sim); 1816 indexSearcherClose(oldSearcher); 1817 } 1818 } 1819 } catch (Exception e) { 1820 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_REOPEN_1, getName()), e); 1821 } 1822 } else { 1823 // make sure we end up with an open index searcher / reader 1824 indexSearcherOpen(getPath()); 1825 } 1826 } 1827 1828 /** 1829 * Checks if the document is in the time range specified in the search parameters.<p> 1830 * 1831 * The creation date and/or the last modification date are checked.<p> 1832 * 1833 * @param doc the document to check the dates against the given time range 1834 * @param params the search parameters where the time ranges are specified 1835 * 1836 * @return true if document is in time range or not time range set otherwise false 1837 */ 1838 protected boolean isInTimeRange(Document doc, CmsSearchParameters params) { 1839 1840 if (!isCheckingTimeRange()) { 1841 // time range check disabled 1842 return true; 1843 } 1844 1845 try { 1846 // check the creation date of the document against the given time range 1847 Date dateCreated = DateTools.stringToDate(doc.getField(CmsSearchField.FIELD_DATE_CREATED).stringValue()); 1848 if (dateCreated.getTime() < params.getMinDateCreated()) { 1849 return false; 1850 } 1851 if (dateCreated.getTime() > params.getMaxDateCreated()) { 1852 return false; 1853 } 1854 1855 // check the last modification date of the document against the given time range 1856 Date dateLastModified = DateTools.stringToDate( 1857 doc.getField(CmsSearchField.FIELD_DATE_LASTMODIFIED).stringValue()); 1858 if (dateLastModified.getTime() < params.getMinDateLastModified()) { 1859 return false; 1860 } 1861 if (dateLastModified.getTime() > params.getMaxDateLastModified()) { 1862 return false; 1863 } 1864 1865 } catch (ParseException ex) { 1866 // date could not be parsed -> doc is in time range 1867 } 1868 1869 return true; 1870 } 1871 1872 /** 1873 * Checks if the score for the results must be calculated based on the provided sort option.<p> 1874 * 1875 * Since Lucene 3 apparently the score is no longer calculated by default, but only if the 1876 * searcher is explicitly told so. This methods checks if, based on the given sort, 1877 * the score must be calculated.<p> 1878 * 1879 * @param searcher the index searcher to prepare 1880 * @param sort the sort option to use 1881 * 1882 * @return true if the sort option should be used 1883 */ 1884 protected boolean isSortScoring(IndexSearcher searcher, Sort sort) { 1885 1886 boolean doScoring = false; 1887 if (sort != null) { 1888 if ((sort == CmsSearchParameters.SORT_DEFAULT) || (sort == CmsSearchParameters.SORT_TITLE)) { 1889 // these default sorts do need score calculation 1890 doScoring = true; 1891 } else if ((sort == CmsSearchParameters.SORT_DATE_CREATED) 1892 || (sort == CmsSearchParameters.SORT_DATE_LASTMODIFIED)) { 1893 // these default sorts don't need score calculation 1894 doScoring = false; 1895 } else { 1896 // for all non-defaults: check if the score field is present, in that case we must calculate the score 1897 SortField[] fields = sort.getSort(); 1898 for (SortField field : fields) { 1899 if (field == SortField.FIELD_SCORE) { 1900 doScoring = true; 1901 break; 1902 } 1903 } 1904 } 1905 } 1906 return doScoring; 1907 } 1908 1909 /** 1910 * Checks if the OpenCms resource referenced by the result document needs to be checked.<p> 1911 * 1912 * @param doc the search result document to check 1913 * 1914 * @return <code>true</code> if the document needs to be checked <code>false</code> otherwise 1915 */ 1916 protected boolean needsPermissionCheck(I_CmsSearchDocument doc) { 1917 1918 if (!isCheckingPermissions()) { 1919 // no permission check is performed at all 1920 return false; 1921 } 1922 1923 if ((doc.getType() == null) || (doc.getPath() == null)) { 1924 // permission check needs only to be performed for VFS documents that contain both fields 1925 return false; 1926 } 1927 1928 if (!I_CmsSearchDocument.VFS_DOCUMENT_KEY_PREFIX.equals(doc.getType()) 1929 && !OpenCms.getResourceManager().hasResourceType(doc.getType())) { 1930 // this is an unknown VFS resource type (also not the generic "VFS" type of OpenCms before 7.0) 1931 return false; 1932 } 1933 return true; 1934 } 1935 1936 /** 1937 * Removes the given backup folder of this index.<p> 1938 * 1939 * @param path the backup folder to remove 1940 */ 1941 protected void removeIndexBackup(String path) { 1942 1943 if (!isBackupReindexing()) { 1944 // if no backup is generated we don't need to do anything 1945 return; 1946 } 1947 1948 // check if the target directory already exists 1949 File file = new File(path); 1950 if (!file.exists()) { 1951 // index does not exist yet 1952 return; 1953 } 1954 try { 1955 FSDirectory dir = FSDirectory.open(file.toPath()); 1956 dir.close(); 1957 CmsFileUtil.purgeDirectory(file); 1958 } catch (Exception e) { 1959 LOG.error(Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_REMOVE_2, getName(), path), e); 1960 } 1961 } 1962 1963 /** 1964 * Generates the uninverting map and adds it to the field configuration. 1965 * @return the generated uninverting map 1966 * 1967 * @see CmsSearchField#addUninvertingMappings(Map) 1968 */ 1969 private Map<String, Type> createUninvertingMap() { 1970 1971 Map<String, UninvertingReader.Type> uninvertingMap = new HashMap<String, UninvertingReader.Type>(); 1972 CmsSearchField.addUninvertingMappings(uninvertingMap); 1973 getFieldConfiguration().addUninvertingMappings(uninvertingMap); 1974 return uninvertingMap; 1975 } 1976 1977}