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