001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.search.fields; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsProperty; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsUser; 035import org.opencms.file.I_CmsResource; 036import org.opencms.i18n.CmsMessageContainer; 037import org.opencms.main.CmsLog; 038import org.opencms.main.CmsRuntimeException; 039import org.opencms.search.CmsSearchUtil; 040import org.opencms.search.Messages; 041import org.opencms.search.extractors.I_CmsExtractionResult; 042import org.opencms.util.CmsStringUtil; 043import org.opencms.xml.CmsXmlUtils; 044 045import java.text.ParseException; 046import java.util.ArrayList; 047import java.util.Arrays; 048import java.util.Comparator; 049import java.util.Date; 050import java.util.List; 051import java.util.Locale; 052import java.util.Map; 053import java.util.SortedMap; 054import java.util.TreeMap; 055 056import org.apache.commons.logging.Log; 057import org.apache.lucene.document.DateTools; 058 059/** 060 * Describes a mapping of a piece of content from an OpenCms VFS resource to a field of a search index.<p> 061 * 062 * @since 7.0.0 063 */ 064public class CmsSearchFieldMapping implements I_CmsSearchFieldMapping { 065 066 /** The log object for this class. */ 067 private static final Log LOG = CmsLog.getLog(CmsSearchFieldMapping.class); 068 069 /** Default for expiration date since Long.MAX_VALUE is to big. */ 070 private static final String DATE_EXPIRED_DEFAULT_STR = "21000101"; 071 072 /** The default expiration date. */ 073 private static Date m_defaultDateExpired; 074 075 /** Serial version UID. */ 076 private static final long serialVersionUID = 3016384419639743033L; 077 078 /** The configured default value. */ 079 private String m_defaultValue; 080 081 /** Pre-calculated hash value. */ 082 private int m_hashCode; 083 084 /** The locale to extract content items in. */ 085 protected Locale m_locale; 086 087 /** The parameter for the mapping type. */ 088 private String m_param; 089 090 /** The mapping type. */ 091 private CmsSearchFieldMappingType m_type; 092 093 /** Flag, indicating if the mapping applies to a lucene index. */ 094 private boolean m_isLucene; 095 096 /** 097 * Public constructor for a new search field mapping.<p> 098 */ 099 public CmsSearchFieldMapping() { 100 101 // no initialization required 102 } 103 104 /** 105 * Public constructor for a new search field mapping.<p> 106 * 107 * @param isLucene flag, indicating if the mapping is done for a lucene index 108 */ 109 public CmsSearchFieldMapping(boolean isLucene) { 110 111 this(); 112 m_isLucene = isLucene; 113 } 114 115 /** 116 * Public constructor for a new search field mapping.<p> 117 * 118 * @param type the type to use, see {@link #setType(CmsSearchFieldMappingType)} 119 * @param param the mapping parameter, see {@link #setParam(String)} 120 */ 121 public CmsSearchFieldMapping(CmsSearchFieldMappingType type, String param) { 122 123 this(); 124 setType(type); 125 setParam(param); 126 } 127 128 /** 129 * Public constructor for a new search field mapping.<p> 130 * 131 * @param type the type to use, see {@link #setType(CmsSearchFieldMappingType)} 132 * @param param the mapping parameter, see {@link #setParam(String)} 133 * @param isLucene flag, indicating if the mapping is done for a lucene index 134 */ 135 public CmsSearchFieldMapping(CmsSearchFieldMappingType type, String param, boolean isLucene) { 136 137 this(type, param); 138 m_isLucene = isLucene; 139 } 140 141 /** 142 * Returns the default expiration date, meaning the resource never expires.<p> 143 * 144 * @return the default expiration date 145 * 146 * @throws ParseException if something goes wrong parsing the default date string 147 */ 148 public static Date getDefaultDateExpired() throws ParseException { 149 150 if (m_defaultDateExpired == null) { 151 m_defaultDateExpired = DateTools.stringToDate("21000101"); 152 } 153 return m_defaultDateExpired; 154 } 155 156 /** 157 * Two mappings are equal if the type and the parameter is equal.<p> 158 * 159 * @see java.lang.Object#equals(java.lang.Object) 160 */ 161 @Override 162 public boolean equals(Object obj) { 163 164 if (obj == this) { 165 return true; 166 } 167 if ((obj instanceof I_CmsSearchFieldMapping)) { 168 I_CmsSearchFieldMapping other = (I_CmsSearchFieldMapping)obj; 169 return (CmsStringUtil.isEqual(m_type, other.getType())) 170 && (CmsStringUtil.isEqual(m_param, other.getParam())); 171 } 172 return false; 173 } 174 175 /** 176 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getDefaultValue() 177 */ 178 public String getDefaultValue() { 179 180 return m_defaultValue; 181 } 182 183 /** 184 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getParam() 185 */ 186 public String getParam() { 187 188 return m_param; 189 } 190 191 /** 192 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getStringValue(org.opencms.file.CmsObject, org.opencms.file.CmsResource, org.opencms.search.extractors.I_CmsExtractionResult, java.util.List, java.util.List) 193 */ 194 public String getStringValue( 195 CmsObject cms, 196 CmsResource res, 197 I_CmsExtractionResult extractionResult, 198 List<CmsProperty> properties, 199 List<CmsProperty> propertiesSearched) { 200 201 String content = null; 202 switch (getType().getMode()) { 203 case 0: // content 204 if (extractionResult != null) { 205 content = extractionResult.getContent(); 206 } 207 break; 208 case 1: // property 209 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) { 210 content = CmsProperty.get(getParam(), properties).getValue(); 211 CmsSearchUtil.stripHtmlFromPropertyIfNecessary(getParam(), content); 212 } 213 break; 214 case 2: // property-search 215 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) { 216 content = CmsProperty.get(getParam(), propertiesSearched).getValue(); 217 CmsSearchUtil.stripHtmlFromPropertyIfNecessary(getParam(), content); 218 } 219 break; 220 case 3: // item (retrieve value for the given XPath from the content items) 221 if ((extractionResult != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) { 222 Map<String, String> localizedContentItems = m_locale == null 223 ? extractionResult.getContentItems() 224 : extractionResult.getContentItems(m_locale); 225 content = getContentItemForXPath(localizedContentItems, getParam().trim()); 226 } 227 break; 228 case 5: // attribute 229 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) { 230 I_CmsResource.CmsResourceAttribute attribute = null; 231 try { 232 attribute = I_CmsResource.CmsResourceAttribute.valueOf(getParam()); 233 } catch (Exception e) { 234 // invalid attribute name specified, attribute will be null 235 } 236 if (attribute != null) { 237 // map all attributes for a resource 238 switch (attribute) { 239 case dateContent: 240 content = m_isLucene 241 ? DateTools.timeToString(res.getDateContent(), DateTools.Resolution.MILLISECOND) 242 : Long.toString(res.getDateContent()); 243 break; 244 case dateCreated: 245 content = m_isLucene 246 ? DateTools.timeToString(res.getDateCreated(), DateTools.Resolution.MILLISECOND) 247 : Long.toString(res.getDateCreated()); 248 break; 249 case dateExpired: 250 if (m_isLucene) { 251 long expirationDate = res.getDateExpired(); 252 if (expirationDate == CmsResource.DATE_EXPIRED_DEFAULT) { 253 // default of Long.MAX_VALUE is to big, use January 1, 2100 instead 254 content = DATE_EXPIRED_DEFAULT_STR; 255 } else { 256 content = DateTools.timeToString( 257 expirationDate, 258 DateTools.Resolution.MILLISECOND); 259 } 260 } else { 261 content = Long.toString(res.getDateExpired()); 262 } 263 break; 264 case dateLastModified: 265 content = m_isLucene 266 ? DateTools.timeToString(res.getDateLastModified(), DateTools.Resolution.MILLISECOND) 267 : Long.toString(res.getDateLastModified()); 268 break; 269 case dateReleased: 270 content = m_isLucene 271 ? DateTools.timeToString(res.getDateReleased(), DateTools.Resolution.MILLISECOND) 272 : Long.toString(res.getDateReleased()); 273 break; 274 case flags: 275 content = String.valueOf(res.getFlags()); 276 break; 277 case length: 278 content = String.valueOf(res.getLength()); 279 break; 280 case name: 281 content = res.getName(); 282 break; 283 case projectLastModified: 284 try { 285 CmsProject project = cms.readProject(res.getProjectLastModified()); 286 content = project.getName(); 287 } catch (Exception e) { 288 // NOOP, content is already null 289 } 290 break; 291 case resourceId: 292 content = res.getResourceId().toString(); 293 break; 294 case rootPath: 295 content = res.getRootPath(); 296 break; 297 case siblingCount: 298 content = String.valueOf(res.getSiblingCount()); 299 break; 300 case state: 301 content = res.getState().toString(); 302 break; 303 case structureId: 304 content = res.getStructureId().toString(); 305 break; 306 case typeId: 307 content = String.valueOf(res.getTypeId()); 308 break; 309 case userCreated: 310 try { 311 CmsUser user = cms.readUser(res.getUserCreated()); 312 content = user.getName(); 313 } catch (Exception e) { 314 // NOOP, content is already null 315 } 316 break; 317 case userLastModified: 318 try { 319 CmsUser user = cms.readUser(res.getUserLastModified()); 320 content = user.getName(); 321 } catch (Exception e) { 322 // NOOP, content is already null 323 } 324 break; 325 case version: 326 content = String.valueOf(res.getVersion()); 327 break; 328 default: 329 // NOOP, content is already null 330 } 331 } 332 } 333 break; 334 default: 335 // NOOP, content is already null 336 } 337 if (content == null) { 338 // in case the content is not available, use the default value for this mapping 339 content = getDefaultValue(); 340 } 341 return content; 342 } 343 344 /** 345 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getType() 346 */ 347 public CmsSearchFieldMappingType getType() { 348 349 return m_type; 350 } 351 352 /** 353 * The hash code depends on the type and the parameter.<p> 354 * 355 * @see java.lang.Object#hashCode() 356 */ 357 @Override 358 public int hashCode() { 359 360 if (m_hashCode == 0) { 361 int hashCode = 73 * (m_type == null ? 29 : m_type.hashCode()); 362 if (m_param != null) { 363 hashCode += m_param.hashCode(); 364 } 365 m_hashCode = hashCode; 366 } 367 return m_hashCode; 368 } 369 370 /** 371 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setDefaultValue(java.lang.String) 372 */ 373 public void setDefaultValue(String defaultValue) { 374 375 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(defaultValue)) { 376 m_defaultValue = defaultValue.trim(); 377 } else { 378 m_defaultValue = null; 379 } 380 } 381 382 /** 383 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setLocale(java.util.Locale) 384 */ 385 public void setLocale(Locale locale) { 386 387 m_locale = locale; 388 } 389 390 /** 391 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setParam(java.lang.String) 392 */ 393 public void setParam(String param) { 394 395 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(param)) { 396 m_param = param.trim(); 397 } else { 398 m_param = null; 399 } 400 } 401 402 /** 403 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setType(org.opencms.search.fields.CmsSearchFieldMappingType) 404 */ 405 public void setType(CmsSearchFieldMappingType type) { 406 407 m_type = type; 408 } 409 410 /** 411 * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setType(java.lang.String) 412 */ 413 public void setType(String type) { 414 415 CmsSearchFieldMappingType mappingType = CmsSearchFieldMappingType.valueOf(type); 416 if (mappingType == null) { 417 // invalid mapping type has been used, throw an exception 418 throw new CmsRuntimeException( 419 new CmsMessageContainer(Messages.get(), Messages.ERR_FIELD_TYPE_UNKNOWN_1, new Object[] {type})); 420 } 421 setType(mappingType); 422 } 423 424 /** 425 * Returns a "\n" separated String of values for the given XPath if according content items can be found.<p> 426 * 427 * @param contentItems the content items to get the value from 428 * @param xpath the short XPath parameter to get the value for 429 * 430 * @return a "\n" separated String of element values found in the content items for the given XPath 431 */ 432 private String getContentItemForXPath(Map<String, String> contentItems, String xpath) { 433 434 if (contentItems.get(xpath) != null) { // content item found for XPath 435 return contentItems.get(xpath); 436 } else { // try a multiple value mapping and ensure that the values are in correct order. 437 SortedMap<List<Integer>, String> valueMap = new TreeMap<>(new Comparator<List<Integer>>() { 438 439 // expects lists of the same length that contain only non-null values. This is given for the use case. 440 @SuppressWarnings("boxing") 441 public int compare(List<Integer> l1, List<Integer> l2) { 442 443 for (int i = 0; i < l1.size(); i++) { 444 int numCompare = Integer.compare(l1.get(i), l2.get(i)); 445 if (0 != numCompare) { 446 return numCompare; 447 } 448 } 449 return 0; 450 } 451 }); 452 for (Map.Entry<String, String> entry : contentItems.entrySet()) { 453 if (CmsXmlUtils.removeXpath(entry.getKey()).equals(xpath)) { // the removed path refers an item 454 455 String[] xPathParts = entry.getKey().split("/"); 456 List<Integer> indexes = new ArrayList<>(xPathParts.length); 457 for (String xPathPart : Arrays.asList(xPathParts)) { 458 if (!xPathPart.isEmpty()) { 459 indexes.add(Integer.valueOf(CmsXmlUtils.getXpathIndexInt(xPathPart))); 460 } 461 } 462 valueMap.put(indexes, entry.getValue()); 463 } 464 } 465 StringBuffer result = new StringBuffer(); 466 for (String value : valueMap.values()) { 467 result.append(value); 468 result.append("\n"); 469 } 470 return result.length() > 1 ? result.toString().substring(0, result.length() - 1) : null; 471 } 472 } 473}