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.ArrayList; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang3.ArrayUtils; 029import org.apache.commons.lang3.StringUtils; 030 031import org.ametys.cms.content.ContentHelper; 032import org.ametys.cms.contenttype.ContentConstants; 033import org.ametys.cms.contenttype.ContentType; 034import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 035import org.ametys.cms.contenttype.ContentTypesHelper; 036import org.ametys.cms.contenttype.MetadataDefinition; 037import org.ametys.cms.contenttype.MetadataType; 038import org.ametys.cms.contenttype.RepeaterDefinition; 039import org.ametys.cms.contenttype.indexing.CustomIndexingField; 040import org.ametys.cms.contenttype.indexing.DefaultMetadataIndexingField; 041import org.ametys.cms.contenttype.indexing.IndexingField; 042import org.ametys.cms.contenttype.indexing.IndexingModel; 043import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 044import org.ametys.cms.repository.Content; 045import org.ametys.cms.search.SearchField; 046import org.ametys.cms.search.model.SystemProperty; 047import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 048import org.ametys.cms.search.solr.field.BooleanSearchField; 049import org.ametys.cms.search.solr.field.DateSearchField; 050import org.ametys.cms.search.solr.field.DoubleSearchField; 051import org.ametys.cms.search.solr.field.LongSearchField; 052import org.ametys.cms.search.solr.field.StringSearchField; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.UnknownAmetysObjectException; 055import org.ametys.plugins.repository.metadata.UnknownMetadataException; 056import org.ametys.runtime.parameter.Enumerator; 057import org.ametys.runtime.plugin.component.AbstractLogEnabled; 058 059/** 060 * Component which helps content searching by providing a simple way to access 061 * content properties (either metadata or system properties). 062 */ 063public class ContentSearchHelper extends AbstractLogEnabled implements Component, Serviceable 064{ 065 066 /** The component role. */ 067 public static final String ROLE = ContentSearchHelper.class.getName(); 068 069 /** The content type extension point. */ 070 protected ContentTypeExtensionPoint _cTypeEP; 071 072 /** The content type helper. */ 073 protected ContentTypesHelper _cTypeHelper; 074 075 /** The content helper. */ 076 protected ContentHelper _contentHelper; 077 078 /** The system property extension point. */ 079 protected SystemPropertyExtensionPoint _sysPropEP; 080 081 /** The ametys object resolver. */ 082 protected AmetysObjectResolver _resolver; 083 084 @Override 085 public void service(ServiceManager manager) throws ServiceException 086 { 087 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 088 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 089 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 090 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 091 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 092 } 093 094 /** 095 * Get the metadata indexing field for the "title" standard metadata. 096 * @return The standard title metadata indexing field. 097 */ 098 public MetadataIndexingField getTitleMetadataIndexingField() 099 { 100 return new DefaultMetadataIndexingField("title", ContentTypesHelper.getTitleMetadataDefinition(), "title"); 101 } 102 103 /** 104 * Get a {@link SearchField} corresponding to a metadata. 105 * @param metadataPath The metadata path. 106 * @param metadataType The metadata type. 107 * @return the search field. 108 */ 109 public SearchField getMetadataSearchField(String metadataPath, MetadataType metadataType) 110 { 111 switch (metadataType) 112 { 113 case STRING: 114 case CONTENT: 115 case SUB_CONTENT: 116 case USER: 117 return new StringSearchField(metadataPath); 118 case LONG: 119 return new LongSearchField(metadataPath); 120 case DOUBLE: 121 return new DoubleSearchField(metadataPath); 122 case BOOLEAN: 123 return new BooleanSearchField(metadataPath); 124 case DATE: 125 case DATETIME: 126 return new DateSearchField(metadataPath); 127 case COMPOSITE: 128 case BINARY: 129 case FILE: 130 case RICH_TEXT: 131 case REFERENCE: 132 default: 133 return null; 134 } 135 } 136 137 /** 138 * Get a {@link SearchField} from a field name in a batch of content types. 139 * @param contentTypes The content types, can be empty to search on any content type. 140 * In that case, only the title metadata and system properties will be usable in sort and facets specs. 141 * @param fieldPath The field path, can be either a system property ID or a indexing field name or path (not joined). 142 * @return The {@link SearchField} corresponding to the 143 */ 144 public SearchField getSearchField(Collection<String> contentTypes, String fieldPath) 145 { 146 SearchField searchField = null; 147 148 if (_sysPropEP.hasExtension(fieldPath)) 149 { 150 SystemProperty property = _sysPropEP.getExtension(fieldPath); 151 searchField = property.getSearchField(); 152 } 153 else 154 { 155 searchField = getIndexingModelSearchField(fieldPath, contentTypes); 156 } 157 158 if (searchField == null) 159 { 160 throw new IllegalArgumentException("The field '" + fieldPath + "' can't be found in the selected content types."); 161 } 162 163 return searchField; 164 } 165 166 /** 167 * Get the {@link SearchField} corresponding to the indexing field at the given path in the indexing model. 168 * @param fieldPath The field path. 169 * @param contentTypes The target content types. 170 * @return The corresponding search field. 171 */ 172 protected SearchField getIndexingModelSearchField(String fieldPath, Collection<String> contentTypes) 173 { 174 String commonContentTypeId = _cTypeHelper.getCommonAncestor(contentTypes); 175 176 MetadataType type = null; 177 178 if (commonContentTypeId != null) 179 { 180 String[] pathSegments = StringUtils.split(fieldPath, ContentConstants.METADATA_PATH_SEPARATOR); 181 String fieldName = pathSegments[0]; 182 183 // Indexing field. 184 ContentType cType = _cTypeEP.getExtension(commonContentTypeId); 185 IndexingField indexingField = cType.getIndexingModel().getField(fieldName); 186 187 if (indexingField == null) 188 { 189 throw new IllegalArgumentException("Search field with path '" + fieldPath + "' refers to an unknown indexing field: " + fieldName); 190 } 191 192 if (indexingField instanceof MetadataIndexingField) 193 { 194 List<String> joinPaths = new ArrayList<>(); 195 String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0]; 196 MetadataDefinition def = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, false); 197 198 if (!joinPaths.isEmpty()) 199 { 200 throw new IllegalArgumentException("The metadata '" + fieldPath + "' can't be used as it is joined."); 201 } 202 203 type = def.getType(); 204 } 205 else if (indexingField instanceof CustomIndexingField) 206 { 207 type = indexingField.getType(); 208 } 209 } 210 else if (fieldPath.equals("title")) 211 { 212 // No specific content type: allow only title. 213 type = ContentTypesHelper.getTitleMetadataDefinition().getType(); 214 } 215 216 return type != null ? getMetadataSearchField(fieldPath, type) : null; 217 } 218 219 /** 220 * Get the metadata definition from the indexing field and compute the join paths. Can be null if the last indexing field is a custom indexing field. 221 * @param indexingField The initial indexing field 222 * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field 223 * @param joinPaths The consecutive's path in case of joint to access the field/metadata 224 * @param addLast <code>true</code> to add the last join path element to the list, <code>false</code> otherwise. 225 * @return The metadata definition or null if not found 226 */ 227 public MetadataDefinition getMetadataDefinition(MetadataIndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths, boolean addLast) 228 { 229 StringBuilder currentMetaPath = new StringBuilder(); 230 currentMetaPath.append(indexingField.getName()); 231 232 MetadataDefinition definition = indexingField.getMetadataDefinition(); 233 234 for (int i = 0; i < remainingPathSegments.length && definition != null; i++) 235 { 236 if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT) 237 { 238 // Add path to content from current content type to join paths. 239 // Join paths are the consecutive metadata paths (separated with '/') to access 240 // the searched content, for instance [address/city, links/department]. 241 joinPaths.add(currentMetaPath.toString()); 242 243 String refCTypeId = definition.getContentType(); 244 if (refCTypeId != null) 245 { 246 if (!_cTypeEP.hasExtension(refCTypeId)) 247 { 248 throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' references an unknown content type:" + refCTypeId); 249 } 250 251 ContentType refCType = _cTypeEP.getExtension(refCTypeId); 252 IndexingModel refIndexingModel = refCType.getIndexingModel(); 253 254 IndexingField refIndexingField = refIndexingModel.getField(remainingPathSegments[i]); 255 if (refIndexingField == null) 256 { 257 throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + remainingPathSegments[i]); 258 } 259 if (refIndexingField instanceof MetadataIndexingField) 260 { 261 throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + remainingPathSegments[i]); 262 } 263 264 return getMetadataDefinition((MetadataIndexingField) refIndexingField, ArrayUtils.subarray(remainingPathSegments, i + 1, remainingPathSegments.length), joinPaths, addLast); 265 } 266 else if ("title".equals(remainingPathSegments[i])) 267 { 268 // No specific content type: allow only title. 269 return ContentTypesHelper.getTitleMetadataDefinition(); 270 } 271 } 272 else 273 { 274 if (definition instanceof RepeaterDefinition) 275 { 276 // Add path to repeater from current content type or last repeater to join paths 277 joinPaths.add(currentMetaPath.toString()); 278 currentMetaPath = new StringBuilder(); 279 currentMetaPath.append(remainingPathSegments[i]); 280 } 281 else 282 { 283 currentMetaPath.append(ContentConstants.METADATA_PATH_SEPARATOR).append(remainingPathSegments[i]); 284 } 285 definition = definition.getMetadataDefinition(remainingPathSegments[i]); 286 } 287 } 288 289 if (addLast) 290 { 291 joinPaths.add(currentMetaPath.toString()); 292 } 293 294 return definition; 295 } 296 297 /** 298 * Get the values of a Content's metadata (can be in another content). 299 * @param content The Content. 300 * @param fullMetadataPath The full metadata path, can represent a metadata in a joined content. 301 * @param type The metadata type. 302 * @param multiple If the metadata is multiple. 303 * @param enumerator The metadata enumerator. 304 * @param full True to generate full representation of metadata, false otherwise. 305 * @return The values of the content metadata. 306 */ 307 public Object getMetadataValues(Content content, String fullMetadataPath, MetadataType type, boolean multiple, Enumerator enumerator, boolean full) 308 { 309 Object value = null; 310 311 try 312 { 313 List<Object> values = _contentHelper.getMetadataValues(content, fullMetadataPath); 314 315 if (!values.isEmpty()) 316 { 317 if (full) 318 { 319 if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) 320 { 321 // If full, extract content values as Maps. If not, the values are kept as content IDs. 322 List<Map<String, Object>> contentValues = getContentValues(values); 323 values = new ArrayList<>(contentValues); 324 } 325 else if (enumerator != null) 326 { 327 // If full, extract enumerated values as Maps. If not, the values are kept as entry IDs. 328 List<Map<String, Object>> enumValues = getEnumeratedValues(values, enumerator); 329 values = new ArrayList<>(enumValues); 330 } 331 } 332 333 if (multiple) 334 { 335 value = values; 336 } 337 else 338 { 339 value = values.get(0); 340 } 341 } 342 } 343 catch (UnknownMetadataException e) 344 { 345 // Ignore, just return a null value. 346 } 347 348 return value; 349 } 350 351 /** 352 * Transform the list of enumerated values into a list of info (as Maps). 353 * @param values The original list of values. 354 * @param enumerator The enumerator. 355 * @return The list of info (value + label) for each value. 356 */ 357 protected List<Map<String, Object>> getEnumeratedValues(List<Object> values, Enumerator enumerator) 358 { 359 List<Map<String, Object>> enumValues = new ArrayList<>(); 360 361 for (Object value : values) 362 { 363 Map<String, Object> entryInfo = new HashMap<>(); 364 entryInfo.put("value", value); 365 366 try 367 { 368 entryInfo.put("label", enumerator.getEntry(value.toString())); 369 } 370 catch (Exception e) 371 { 372 entryInfo.put("label", value); 373 } 374 375 enumValues.add(entryInfo); 376 } 377 378 return enumValues; 379 } 380 381 /** 382 * Get information for UI on a content metadata 383 * @param values The list of content IDs. 384 * @return The informations 385 */ 386 protected List<Map<String, Object>> getContentValues(List<Object> values) 387 { 388 List<Map<String, Object>> contents = new ArrayList<>(); 389 for (Object contentId : values) 390 { 391 Map<String, Object> contentInfo = new HashMap<>(); 392 contentInfo.put("id", contentId); 393 394 try 395 { 396 Content refContent = _resolver.resolveById((String) contentId); 397 contentInfo.put("title", refContent.getTitle()); 398 contentInfo.put("isSimple", _isSimple(refContent)); 399 } 400 catch (UnknownAmetysObjectException e) 401 { 402 // Nothing 403 } 404 405 contents.add(contentInfo); 406 } 407 408 return contents; 409 } 410 411 private boolean _isSimple(Content content) 412 { 413 for (String cTypeId : content.getTypes()) 414 { 415 ContentType cType = _cTypeEP.getExtension(cTypeId); 416 if (!cType.isSimple()) 417 { 418 return false; 419 } 420 } 421 return true; 422 } 423 424}