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