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.ContentType; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.repository.Content; 039import org.ametys.cms.search.model.ResultField; 040import org.ametys.cms.search.model.SearchModel; 041import org.ametys.cms.search.model.SystemProperty; 042import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 043import org.ametys.runtime.model.ModelItem; 044import org.ametys.runtime.model.ModelItemContainer; 045import org.ametys.runtime.model.ModelItemGroup; 046import org.ametys.runtime.plugin.component.AbstractLogEnabled; 047 048/** 049 * Component creating content values extractors from {@link SearchModel}s or content type IDs. 050 */ 051public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable 052{ 053 054 /** The component role. */ 055 public static final String ROLE = ContentValuesExtractorFactory.class.getName(); 056 057 /** The content type extension point. */ 058 protected ContentTypeExtensionPoint _cTypeEP; 059 060 /** The content type helper. */ 061 protected ContentTypesHelper _cTypeHelper; 062 063 /** The content type helper. */ 064 protected ContentSearchHelper _searchHelper; 065 066 /** The system property extension point. */ 067 protected SystemPropertyExtensionPoint _sysPropEP; 068 069 @Override 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 073 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 074 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 075 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 076 } 077 078 /** 079 * Create a ContentValuesExtractor from a search model. 080 * @param searchModel The reference search model. 081 * @return a ContentValuesExtractor backed by the given search model. 082 */ 083 public SearchModelContentValuesExtractor create(SearchModel searchModel) 084 { 085 return new SearchModelContentValuesExtractor(searchModel); 086 } 087 088 /** 089 * Create a simple ContentValuesExtractor from a list of content types. 090 * @param contentTypes The content types to search on. 091 * @return a ContentValuesExtractor referencing the given content types. 092 */ 093 public SimpleContentValuesExtractor create(Collection<String> contentTypes) 094 { 095 return create(contentTypes, Collections.emptyList()); 096 } 097 098 /** 099 * Create a simple ContentValuesExtractor from a list of content types. 100 * @param contentTypes The content types to search on. 101 * @param fields The fields to extract. 102 * @return a ContentValuesExtractor referencing the given content types. 103 */ 104 public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields) 105 { 106 return new SimpleContentValuesExtractor(contentTypes, fields); 107 } 108 109 /** 110 * A ContentValuesExtractor backed by a {@link SearchModel}. 111 */ 112 public class SearchModelContentValuesExtractor 113 { 114 private SearchModel _searchModel; 115 private boolean _fullValues; 116 117 /** 118 * Build a ContentValuesExtractor referencing a {@link SearchModel}. 119 * @param searchModel the {@link SearchModel}. 120 */ 121 public SearchModelContentValuesExtractor(SearchModel searchModel) 122 { 123 _searchModel = searchModel; 124 _fullValues = false; 125 } 126 127 /** 128 * Whether to return full values or not. 129 * @param fullValues true to return full values, false otherwise. 130 * @return The ContentValuesExtractor itself. 131 */ 132 public SearchModelContentValuesExtractor setFullValues(boolean fullValues) 133 { 134 _fullValues = fullValues; 135 return this; 136 } 137 138 /** 139 * Get the values from the given content. 140 * @param content The content. 141 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 142 * @return the extracted values. 143 */ 144 public Map<String, Object> getValues(Content content, Locale defaultLocale) 145 { 146 return getValues(content, defaultLocale, Collections.emptyMap()); 147 } 148 149 /** 150 * Get the values from the given content. 151 * @param content The content. 152 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 153 * @param contextualParameters The search contextual parameters. 154 * @return the extracted values. 155 */ 156 public Map<String, Object> getValues(Content content, Locale defaultLocale, 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, defaultLocale) : field.getValue(content, defaultLocale); 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 if (CollectionUtils.isEmpty(fieldNames)) 218 { 219 return getAllFields(); 220 } 221 else 222 { 223 return getFields(fieldNames); 224 } 225 } 226 227 /** 228 * Retrieves a {@link Map} with all possible fields 229 * @return A {@link Map} with all possible fields 230 */ 231 protected Map<String, Object> getAllFields() 232 { 233 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 234 235 if (_contentTypes.isEmpty()) 236 { 237 // Add only title. 238 fields.put("title", _cTypeHelper.getTitleAttributeDefinition()); 239 } 240 else 241 { 242 // Add all attributes. 243 for (String id : _contentTypes) 244 { 245 fields.putAll(getAllAttributes(_cTypeEP.getExtension(id))); 246 } 247 } 248 249 // Add system properties 250 for (String propName : _sysPropEP.getDisplayProperties()) 251 { 252 fields.put(propName, _sysPropEP.getExtension(propName)); 253 } 254 255 return fields; 256 } 257 258 /** 259 * Retrieves a {@link Map} with the fields corresponding to the given name 260 * @param fieldNames The names of the fields to retrieve 261 * @return a {@link Map} with the fields 262 */ 263 protected Map<String, Object> getFields(List<String> fieldNames) 264 { 265 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 266 267 for (String fieldName : fieldNames) 268 { 269 if (_sysPropEP.hasExtension(fieldName)) 270 { 271 if (_sysPropEP.isDisplayable(fieldName)) 272 { 273 fields.put(fieldName, _sysPropEP.getExtension(fieldName)); 274 } 275 else 276 { 277 throw new IllegalArgumentException("The system property '" + fieldName + "' is not displayable."); 278 } 279 } 280 else 281 { 282 String modelItemPath = fieldName; 283 284 Iterator<String> ids = _contentTypes.iterator(); 285 ModelItem modelItem = null; 286 while (ids.hasNext() && modelItem == null) 287 { 288 ContentType cType = _cTypeEP.getExtension(ids.next()); 289 if (cType.hasModelItem(modelItemPath)) 290 { 291 modelItem = cType.getModelItem(modelItemPath); 292 } 293 } 294 295 // Take the standard title metadata definition if no specific content type is defined. 296 if (modelItem == null && fieldName.equals(Content.ATTRIBUTE_TITLE) && _contentTypes.isEmpty()) 297 { 298 modelItem = _cTypeHelper.getTitleAttributeDefinition(); 299 } 300 301 if (modelItem != null) 302 { 303 fields.put(fieldName, modelItem); 304 } 305 else 306 { 307 throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the given content types."); 308 } 309 } 310 } 311 312 return fields; 313 } 314 315 /** 316 * Retrieve all the attributes present in the given container. 317 * @param container The model item container. 318 * @return All the attributes present in the given container. 319 */ 320 protected Map<String, Object> getAllAttributes(ModelItemContainer container) 321 { 322 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 323 324 for (ModelItem definition : container.getModelItems()) 325 { 326 if (definition instanceof ModelItemGroup) 327 { 328 // Composite or repeater: add nothing at this level and recurse. 329 fields.putAll(getAllAttributes((ModelItemGroup) definition)); 330 } 331 else 332 { 333 // Add the attribute to the fields map. 334 fields.put(definition.getPath(), definition); 335 } 336 } 337 338 return fields; 339 } 340 341 /** 342 * Whether to return full values or not. 343 * @param fullValues true to return full values, false otherwise. 344 * @return The ContentValuesExtractor itself. 345 */ 346 public SimpleContentValuesExtractor setFullValues(boolean fullValues) 347 { 348 _fullValues = fullValues; 349 return this; 350 } 351 352 /** 353 * Get the values from the given content. 354 * @param content The content. 355 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 356 * @return the extracted values. 357 */ 358 public Map<String, Object> getValues(Content content, Locale defaultLocale) 359 { 360 return getValues(content, defaultLocale, Collections.emptyMap()); 361 } 362 363 /** 364 * Get the values from the given content. 365 * @param content The content. 366 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 367 * @param contextualParameters The search contextual parameters. 368 * @return the extracted values. 369 */ 370 public Map<String, Object> getValues(Content content, Locale defaultLocale, 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, defaultLocale); 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 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 394 * @return The value. 395 */ 396 protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale) 397 { 398 Object value = null; 399 400 if (field instanceof SystemProperty) 401 { 402 value = ((SystemProperty) field).getJsonValue(content, _fullValues); 403 } 404 else if (field instanceof ModelItem) 405 { 406 value = _searchHelper.getAttributeValue(content, fieldName, (ModelItem) field, defaultLocale, _fullValues); 407 } 408 409 return value; 410 } 411 } 412}