001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.search.content; 017 018import java.util.Collection; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.collections.CollectionUtils; 034 035import org.ametys.cms.contenttype.ContentConstants; 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.contenttype.ContentTypesHelper; 039import org.ametys.cms.contenttype.MetadataDefinition; 040import org.ametys.cms.contenttype.MetadataDefinitionHolder; 041import org.ametys.cms.contenttype.MetadataType; 042import org.ametys.cms.contenttype.RepeaterDefinition; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.search.model.ResultField; 045import org.ametys.cms.search.model.SearchModel; 046import org.ametys.cms.search.model.SystemProperty; 047import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 048import org.ametys.runtime.parameter.Enumerator; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * Component creating content values extractors from {@link SearchModel}s or content type IDs. 053 */ 054public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable 055{ 056 057 /** The component role. */ 058 public static final String ROLE = ContentValuesExtractorFactory.class.getName(); 059 060 /** The content type extension point. */ 061 protected ContentTypeExtensionPoint _cTypeEP; 062 063 /** The content type helper. */ 064 protected ContentTypesHelper _cTypeHelper; 065 066 /** The content type helper. */ 067 protected ContentSearchHelper _searchHelper; 068 069 /** The system property extension point. */ 070 protected SystemPropertyExtensionPoint _sysPropEP; 071 072 @Override 073 public void service(ServiceManager manager) throws ServiceException 074 { 075 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 076 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 077 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 078 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 079 } 080 081 /** 082 * Create a ContentValuesExtractor from a search model. 083 * @param searchModel The reference search model. 084 * @return a ContentValuesExtractor backed by the given search model. 085 */ 086 public SearchModelContentValuesExtractor create(SearchModel searchModel) 087 { 088 return new SearchModelContentValuesExtractor(searchModel); 089 } 090 091 /** 092 * Create a simple ContentValuesExtractor from a list of content types. 093 * @param contentTypes The content types to search on. 094 * @return a ContentValuesExtractor referencing the given content types. 095 */ 096 public SimpleContentValuesExtractor create(Collection<String> contentTypes) 097 { 098 return create(contentTypes, Collections.emptyList()); 099 } 100 101 /** 102 * Create a simple ContentValuesExtractor from a list of content types. 103 * @param contentTypes The content types to search on. 104 * @param fields The fields to extract. 105 * @return a ContentValuesExtractor referencing the given content types. 106 */ 107 public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields) 108 { 109 return new SimpleContentValuesExtractor(contentTypes, fields); 110 } 111 112 /** 113 * A ContentValuesExtractor backed by a {@link SearchModel}. 114 */ 115 public class SearchModelContentValuesExtractor 116 { 117 private SearchModel _searchModel; 118 private boolean _fullValues; 119 120 /** 121 * Build a ContentValuesExtractor referencing a {@link SearchModel}. 122 * @param searchModel the {@link SearchModel}. 123 */ 124 public SearchModelContentValuesExtractor(SearchModel searchModel) 125 { 126 _searchModel = searchModel; 127 _fullValues = false; 128 } 129 130 /** 131 * Whether to return full values or not. 132 * @param fullValues true to return full values, false otherwise. 133 * @return The ContentValuesExtractor itself. 134 */ 135 public SearchModelContentValuesExtractor setFullValues(boolean fullValues) 136 { 137 _fullValues = fullValues; 138 return this; 139 } 140 141 /** 142 * Get the values from the given content. 143 * @param content The content. 144 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 145 * @return the extracted values. 146 */ 147 public Map<String, Object> getValues(Content content, Locale defaultLocale) 148 { 149 return getValues(content, defaultLocale, Collections.emptyMap()); 150 } 151 152 /** 153 * Get the values from the given content. 154 * @param content The content. 155 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 156 * @param contextualParameters The search contextual parameters. 157 * @return the extracted values. 158 */ 159 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 160 { 161 Map<String, Object> properties = new HashMap<>(); 162 163 Map<String, ? extends ResultField> resultFields = _searchModel.getResultFields(contextualParameters); 164 165 for (ResultField field : resultFields.values()) 166 { 167 Object value = _fullValues ? field.getFullValue(content, defaultLocale) : field.getValue(content, defaultLocale); 168 169 if (value != null) 170 { 171 putPropertyValue(properties, field, value); 172 } 173 } 174 175 return properties; 176 } 177 178 /** 179 * Put a result value at its right place in the properties map. 180 * @param properties the properties map to fill. 181 * @param column the search column. 182 * @param value the result value. 183 */ 184 protected void putPropertyValue(Map<String, Object> properties, ResultField column, Object value) 185 { 186 String id = column.getId(); 187 properties.put(id.replace('.', '/'), value); 188 } 189 190 } 191 192 /** 193 * A simple ContentValuesExtractor on a list of content types. 194 */ 195 public class SimpleContentValuesExtractor 196 { 197 private Set<String> _contentTypes; 198 private Map<String, Object> _fields; 199 private boolean _fullValues; 200 201 /** 202 * Build a simple ContentValuesExtractor on a list of content types. 203 * @param contentTypes The content types, can be empty. 204 * @param fieldNames The field names. 205 */ 206 public SimpleContentValuesExtractor(Collection<String> contentTypes, List<String> fieldNames) 207 { 208 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 209 this._fields = initializeFields(fieldNames); 210 this._fullValues = false; 211 } 212 213 /** 214 * Initialize the fields from the field names. 215 * @param fieldNames The field names. 216 * @return The fields definitions (either MetadataDefinition or SystemProperty), indexed by field name. 217 */ 218 protected Map<String, Object> initializeFields(List<String> fieldNames) 219 { 220 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 221 222 if (CollectionUtils.isEmpty(fieldNames)) 223 { 224 addAllFields(fields); 225 } 226 else 227 { 228 addFields(fields, fieldNames); 229 } 230 231 return fields; 232 } 233 234 /** 235 * Add all possible fields to the map. 236 * @param fields The map of field definitions to fill. 237 */ 238 protected void addAllFields(Map<String, Object> fields) 239 { 240 // Add all metadatas 241 if (_contentTypes.isEmpty()) 242 { 243 // Add only title. 244 fields.put("title", new MetadataReference(ContentTypesHelper.getTitleMetadataDefinition(), false)); 245 } 246 else 247 { 248 // Add all metadatas. 249 for (String id : _contentTypes) 250 { 251 addAllMetadatas(fields, _cTypeEP.getExtension(id), "", false); 252 } 253 } 254 255 // Add system properties 256 for (String propName : _sysPropEP.getDisplayProperties()) 257 { 258 fields.put(propName, _sysPropEP.getExtension(propName)); 259 } 260 } 261 262 /** 263 * Add the given fields to the map. 264 * @param fields The map of field definitions to fill. 265 * @param fieldNames The fields to add. 266 */ 267 protected void addFields(Map<String, Object> fields, List<String> fieldNames) 268 { 269 for (String field : fieldNames) 270 { 271 if (_sysPropEP.hasExtension(field)) 272 { 273 if (_sysPropEP.isDisplayable(field)) 274 { 275 fields.put(field, _sysPropEP.getExtension(field)); 276 } 277 else 278 { 279 throw new IllegalArgumentException("The system property '" + field + "' is not displayable."); 280 } 281 } 282 else 283 { 284 String metadataPath = field; 285 286 Iterator<String> ids = _contentTypes.iterator(); 287 MetadataReference metaRef = null; 288 while (ids.hasNext() && metaRef == null) 289 { 290 ContentType cType = _cTypeEP.getExtension(ids.next()); 291 List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionPath(metadataPath, cType); 292 if (CollectionUtils.isNotEmpty(metaDefs)) 293 { 294 MetadataDefinition metaDef = metaDefs.get(metaDefs.size() - 1); 295 metaRef = new MetadataReference(metaDef, isMultiple(metaDefs)); 296 } 297 } 298 299 // Take the standard title metadata definition if no specific content type is defined. 300 if (metaRef == null && field.equals("title") && _contentTypes.isEmpty()) 301 { 302 metaRef = new MetadataReference(ContentTypesHelper.getTitleMetadataDefinition(), false); 303 } 304 305 if (metaRef != null) 306 { 307 fields.put(field, metaRef); 308 } 309 else 310 { 311 throw new IllegalArgumentException("The field '" + field + "' can't be found in the given content types."); 312 } 313 } 314 } 315 } 316 317 /** 318 * Add all the metadata present in a holder. 319 * @param fields The field map to fill. 320 * @param defHolder The definition holder. 321 * @param prefix The metadata path prefix. 322 * @param parentIsMultiple true if the parent is multiple. 323 */ 324 protected void addAllMetadatas(Map<String, Object> fields, MetadataDefinitionHolder defHolder, String prefix, boolean parentIsMultiple) 325 { 326 for (String name : defHolder.getMetadataNames()) 327 { 328 MetadataDefinition definition = defHolder.getMetadataDefinition(name); 329 String path = prefix + name; 330 // The metadata is considered multiple if any of its parent is multiple or it's itself multiple. 331 boolean multiple = parentIsMultiple || definition.isMultiple() || definition instanceof RepeaterDefinition; 332 333 if (definition.getType() == MetadataType.COMPOSITE) 334 { 335 // Composite or repeater: add nothing at this level and recurse. 336 addAllMetadatas(fields, definition, path + ContentConstants.METADATA_PATH_SEPARATOR, multiple); 337 } 338 else if (!fields.containsKey(path)) 339 { 340 // Add the metadata to the field map (if it's not present already). 341 fields.put(path, new MetadataReference(definition, multiple)); 342 } 343 } 344 } 345 346 /** 347 * Whether to return full values or not. 348 * @param fullValues true to return full values, false otherwise. 349 * @return The ContentValuesExtractor itself. 350 */ 351 public SimpleContentValuesExtractor setFullValues(boolean fullValues) 352 { 353 _fullValues = fullValues; 354 return this; 355 } 356 357 /** 358 * Get the values from the given content. 359 * @param content The content. 360 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 361 * @return the extracted values. 362 */ 363 public Map<String, Object> getValues(Content content, Locale defaultLocale) 364 { 365 return getValues(content, defaultLocale, Collections.emptyMap()); 366 } 367 368 /** 369 * Get the values from the given content. 370 * @param content The content. 371 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 372 * @param contextualParameters The search contextual parameters. 373 * @return the extracted values. 374 */ 375 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 376 { 377 Map<String, Object> properties = new LinkedHashMap<>(); 378 379 for (String fieldName : _fields.keySet()) 380 { 381 Object field = _fields.get(fieldName); 382 383 Object value = getValue(content, fieldName, field, defaultLocale); 384 if (value != null) 385 { 386 properties.put(fieldName, value); 387 } 388 } 389 390 return properties; 391 } 392 393 /** 394 * Get a value from the Content. 395 * @param content The content. 396 * @param fieldName The field name. 397 * @param field The field definition. 398 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 399 * @return The value. 400 */ 401 protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale) 402 { 403 Object value = null; 404 405 if (field instanceof SystemProperty) 406 { 407 value = ((SystemProperty) field).getJsonValue(content, _fullValues); 408 } 409 else if (field instanceof MetadataReference) 410 { 411 String metadataPath = fieldName; 412 MetadataReference metaRef = (MetadataReference) field; 413 value = _searchHelper.getMetadataValue(content, metadataPath, metaRef.getType(), metaRef.isMultiple(), metaRef.getEnumerator(), defaultLocale, _fullValues); 414 } 415 416 return value; 417 } 418 419 /** 420 * Test if any metadata definition in a chain is multiple or a repeater. 421 * @param metaDefs The metadata definition chain. 422 * @return <code>true</code> if any metadata definition in a chain is multiple or a repeater. 423 */ 424 protected boolean isMultiple(List<MetadataDefinition> metaDefs) 425 { 426 for (MetadataDefinition def : metaDefs) 427 { 428 if (def.isMultiple() || def instanceof RepeaterDefinition) 429 { 430 return true; 431 } 432 } 433 434 return false; 435 } 436 437 } 438 439 /** 440 * Represents a metadata definition and a multiple status, which can be different 441 * from the metadata definition's own multiple status. 442 */ 443 class MetadataReference 444 { 445 private MetadataDefinition _definition; 446 447 private boolean _multiple; 448 449 /** 450 * Build a MetadataReference object. 451 * @param definition The reference MetadataDefinition. 452 * @param multiple The multiple status. 453 */ 454 public MetadataReference(MetadataDefinition definition, boolean multiple) 455 { 456 this._definition = definition; 457 this._multiple = multiple; 458 } 459 460 /** 461 * Get the reference MetadataDefinition. 462 * @return the reference MetadataDefinition. 463 */ 464 public MetadataDefinition getDefinition() 465 { 466 return _definition; 467 } 468 469 /** 470 * Get the multiple status. 471 * @return The multiple status. 472 */ 473 public boolean isMultiple() 474 { 475 return _multiple; 476 } 477 478 /** 479 * Get the metadata type. 480 * @return the metadata type. 481 */ 482 public MetadataType getType() 483 { 484 return _definition.getType(); 485 } 486 487 /** 488 * Get the metadata enumerator. 489 * @return the metadata enumerator. 490 */ 491 public Enumerator getEnumerator() 492 { 493 return _definition.getEnumerator(); 494 } 495 } 496}