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