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.MetadataResultField; 040import org.ametys.cms.search.model.ResultField; 041import org.ametys.cms.search.model.SearchModel; 042import org.ametys.cms.search.model.SystemProperty; 043import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 044import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 045import org.ametys.plugins.repository.model.RepeaterDefinition; 046import org.ametys.runtime.model.ModelItem; 047import org.ametys.runtime.model.ModelItemContainer; 048import org.ametys.runtime.model.ModelItemGroup; 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 /** To determine the externalizable status */ 073 protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 074 075 @Override 076 public void service(ServiceManager manager) throws ServiceException 077 { 078 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 079 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 080 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 081 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 082 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 083 } 084 085 /** 086 * Create a ContentValuesExtractor from a search model. 087 * @param searchModel The reference search model. 088 * @return a ContentValuesExtractor backed by the given search model. 089 */ 090 public SearchModelContentValuesExtractor create(SearchModel searchModel) 091 { 092 return new SearchModelContentValuesExtractor(searchModel); 093 } 094 095 /** 096 * Create a simple ContentValuesExtractor from a list of content types. 097 * @param contentTypes The content types to search on. 098 * @return a ContentValuesExtractor referencing the given content types. 099 */ 100 public SimpleContentValuesExtractor create(Collection<String> contentTypes) 101 { 102 return create(contentTypes, Collections.emptyList()); 103 } 104 105 /** 106 * Create a simple ContentValuesExtractor from a list of content types. 107 * @param contentTypes The content types to search on. 108 * @param fields The fields to extract. 109 * @return a ContentValuesExtractor referencing the given content types. 110 */ 111 public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields) 112 { 113 return new SimpleContentValuesExtractor(contentTypes, fields); 114 } 115 116 /** 117 * A ContentValuesExtractor 118 */ 119 public interface ContentValuesExtractor 120 { 121 /** 122 * Whether to return full values or not. 123 * @param fullValues true to return full values, false otherwise. 124 * @return The ContentValuesExtractor itself. 125 */ 126 public ContentValuesExtractor setFullValues(boolean fullValues); 127 128 /** 129 * Get the values from the given content. 130 * @param content The content. 131 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 132 * @return the extracted values. 133 */ 134 public Map<String, Object> getValues(Content content, Locale defaultLocale); 135 136 /** 137 * Get the values from the given content. 138 * @param content The content. 139 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 140 * @param contextualParameters The search contextual parameters. 141 * @return the extracted values. 142 */ 143 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters); 144 } 145 146 /** 147 * A ContentValuesExtractor backed by a {@link SearchModel}. 148 */ 149 public class SearchModelContentValuesExtractor implements ContentValuesExtractor 150 { 151 private SearchModel _searchModel; 152 private boolean _fullValues; 153 154 /** 155 * Build a ContentValuesExtractor referencing a {@link SearchModel}. 156 * @param searchModel the {@link SearchModel}. 157 */ 158 public SearchModelContentValuesExtractor(SearchModel searchModel) 159 { 160 _searchModel = searchModel; 161 _fullValues = false; 162 } 163 164 public SearchModelContentValuesExtractor setFullValues(boolean fullValues) 165 { 166 _fullValues = fullValues; 167 return this; 168 } 169 170 public Map<String, Object> getValues(Content content, Locale defaultLocale) 171 { 172 return getValues(content, defaultLocale, Collections.emptyMap()); 173 } 174 175 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 176 { 177 Map<String, Object> properties = new HashMap<>(); 178 179 Map<String, ? extends ResultField> resultFields = _searchModel.getResultFields(contextualParameters); 180 181 boolean handleExternalizable = (boolean) contextualParameters.getOrDefault("externalizable", true); 182 183 for (ResultField field : resultFields.values()) 184 { 185 Object value = _fullValues ? field.getFullValue(content, defaultLocale) : field.getValue(content, defaultLocale); 186 187 if (handleExternalizable && field instanceof MetadataResultField) 188 { 189 String fieldName = ((MetadataResultField) field).getFieldPath(); 190 191 value = _handleExternalizable(value, content, fieldName); 192 } 193 194 if (value != null) 195 { 196 putPropertyValue(properties, field, value); 197 } 198 } 199 200 return properties; 201 } 202 203 /** 204 * Put a result value at its right place in the properties map. 205 * @param properties the properties map to fill. 206 * @param column the search column. 207 * @param value the result value. 208 */ 209 protected void putPropertyValue(Map<String, Object> properties, ResultField column, Object value) 210 { 211 String id = column.getId(); 212 properties.put(id.replace('.', '/'), value); 213 } 214 215 } 216 217 /** 218 * A simple ContentValuesExtractor on a list of content types. 219 */ 220 public class SimpleContentValuesExtractor implements ContentValuesExtractor 221 { 222 private Set<String> _contentTypes; 223 private Map<String, Object> _fields; 224 private boolean _fullValues; 225 226 /** 227 * Build a simple ContentValuesExtractor on a list of content types. 228 * @param contentTypes The content types, can be empty. 229 * @param fieldNames The field names. 230 */ 231 public SimpleContentValuesExtractor(Collection<String> contentTypes, List<String> fieldNames) 232 { 233 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 234 this._fields = initializeFields(fieldNames); 235 this._fullValues = false; 236 } 237 238 /** 239 * Initialize the fields from the field names. 240 * @param fieldNames The field names. 241 * @return The fields definitions (either MetadataDefinition or SystemProperty), indexed by field name. 242 */ 243 protected Map<String, Object> initializeFields(List<String> fieldNames) 244 { 245 if (CollectionUtils.isEmpty(fieldNames)) 246 { 247 return getAllFields(); 248 } 249 else 250 { 251 return getFields(fieldNames); 252 } 253 } 254 255 /** 256 * Retrieves a {@link Map} with all possible fields 257 * @return A {@link Map} with all possible fields 258 */ 259 protected Map<String, Object> getAllFields() 260 { 261 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 262 263 if (_contentTypes.isEmpty()) 264 { 265 // Add only title. 266 fields.put("title", _cTypeHelper.getTitleAttributeDefinition()); 267 } 268 else 269 { 270 // Add all attributes. 271 for (String id : _contentTypes) 272 { 273 fields.putAll(getAllAttributes(_cTypeEP.getExtension(id))); 274 } 275 } 276 277 // Add system properties 278 for (String propName : _sysPropEP.getDisplayProperties()) 279 { 280 fields.put(propName, _sysPropEP.getExtension(propName)); 281 } 282 283 return fields; 284 } 285 286 /** 287 * Retrieves a {@link Map} with the fields corresponding to the given name 288 * @param fieldNames The names of the fields to retrieve 289 * @return a {@link Map} with the fields 290 */ 291 protected Map<String, Object> getFields(List<String> fieldNames) 292 { 293 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 294 295 for (String fieldName : fieldNames) 296 { 297 if (_sysPropEP.hasExtension(fieldName)) 298 { 299 if (_sysPropEP.isDisplayable(fieldName)) 300 { 301 fields.put(fieldName, _sysPropEP.getExtension(fieldName)); 302 } 303 else 304 { 305 throw new IllegalArgumentException("The system property '" + fieldName + "' is not displayable."); 306 } 307 } 308 else 309 { 310 String modelItemPath = fieldName; 311 312 Iterator<String> ids = _contentTypes.iterator(); 313 ModelItem modelItem = null; 314 while (ids.hasNext() && modelItem == null) 315 { 316 ContentType cType = _cTypeEP.getExtension(ids.next()); 317 if (cType.hasModelItem(modelItemPath)) 318 { 319 modelItem = cType.getModelItem(modelItemPath); 320 } 321 } 322 323 // Take the standard title metadata definition if no specific content type is defined. 324 if (modelItem == null && fieldName.equals(Content.ATTRIBUTE_TITLE) && _contentTypes.isEmpty()) 325 { 326 modelItem = _cTypeHelper.getTitleAttributeDefinition(); 327 } 328 329 if (modelItem != null) 330 { 331 fields.put(fieldName, modelItem); 332 } 333 else 334 { 335 throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the given content types."); 336 } 337 } 338 } 339 340 return fields; 341 } 342 343 /** 344 * Retrieve all the attributes present in the given container. 345 * @param container The model item container. 346 * @return All the attributes present in the given container. 347 */ 348 protected Map<String, Object> getAllAttributes(ModelItemContainer container) 349 { 350 LinkedHashMap<String, Object> fields = new LinkedHashMap<>(); 351 352 for (ModelItem definition : container.getModelItems()) 353 { 354 if (definition instanceof ModelItemGroup) 355 { 356 // Composite or repeater: add nothing at this level and recurse. 357 fields.putAll(getAllAttributes((ModelItemGroup) definition)); 358 } 359 else 360 { 361 // Add the attribute to the fields map. 362 fields.put(definition.getPath(), definition); 363 } 364 } 365 366 return fields; 367 } 368 369 public SimpleContentValuesExtractor setFullValues(boolean fullValues) 370 { 371 _fullValues = fullValues; 372 return this; 373 } 374 375 public Map<String, Object> getValues(Content content, Locale defaultLocale) 376 { 377 return getValues(content, defaultLocale, Collections.emptyMap()); 378 } 379 380 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 381 { 382 Map<String, Object> properties = new LinkedHashMap<>(); 383 384 for (String fieldName : _fields.keySet()) 385 { 386 Object field = _fields.get(fieldName); 387 388 Object value = getValue(content, fieldName, field, defaultLocale, (boolean) contextualParameters.getOrDefault("externalizable", true)); 389 if (value != null) 390 { 391 properties.put(fieldName, value); 392 } 393 } 394 395 return properties; 396 } 397 398 /** 399 * Get a value from the Content. 400 * @param content The content. 401 * @param fieldName The field name. 402 * @param field The field definition. 403 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 404 * @param handleExternalizable false to simply get current value on externalizable, true to get a json object with current value and status 405 * @return The value. 406 */ 407 protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale, boolean handleExternalizable) 408 { 409 Object value = null; 410 411 if (field instanceof SystemProperty) 412 { 413 value = ((SystemProperty) field).getJsonValue(content, _fullValues); 414 } 415 else if (field instanceof ModelItem) 416 { 417 value = _searchHelper.getAttributeValue(content, fieldName, (ModelItem) field, defaultLocale, _fullValues); 418 419 if (handleExternalizable) 420 { 421 value = _handleExternalizable(value, content, fieldName); 422 } 423 } 424 425 return value; 426 } 427 } 428 429 @SuppressWarnings("unchecked") 430 private Object _handleExternalizable(Object value, Content content, String fieldName) 431 { 432 ModelItem definition = content.getDefinition(fieldName); 433 if (definition instanceof RepeaterDefinition) 434 { 435 RepeaterDefinition repeaterDefinition = (RepeaterDefinition) definition; 436 437 Map<String, Object> repeaterValue = (Map<String, Object>) value; 438 if (repeaterValue != null) 439 { 440 List<Map<String, Object>> entries = (List<Map<String, Object>>) repeaterValue.get("entries"); 441 for (int i = 0; i < entries.size(); i++) 442 { 443 Map<String, Object> entry = entries.get(i); 444 445 Map<String, Object> entryValues = (Map<String, Object>) entry.get("values"); 446 447 for (ModelItem modelItem : repeaterDefinition.getChildren()) 448 { 449 String subFieldName = modelItem.getName(); 450 Object newValue = _handleExternalizable(entryValues.get(subFieldName), content, fieldName + "[" + (i + 1) + "]/" + subFieldName); 451 entryValues.put(subFieldName, newValue); 452 } 453 } 454 } 455 456 return value; 457 } 458 else if (_externalizableDataProviderEP.isDataExternalizable(content, definition)) 459 { 460 Map<String, Object> json = new HashMap<>(); 461 json.put("value", value); 462 json.put("status", content.getStatus(fieldName).toString().toLowerCase()); 463 return json; 464 } 465 else 466 { 467 return value; 468 } 469 } 470}