001/* 002 * Copyright 2018 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.web.frontoffice.search.metamodel; 017 018import java.util.Arrays; 019import java.util.Collection; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.Set; 030import java.util.stream.Collectors; 031import java.util.stream.Stream; 032 033import org.apache.avalon.framework.activity.Disposable; 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.configuration.Configuration; 037import org.apache.avalon.framework.configuration.DefaultConfiguration; 038import org.apache.avalon.framework.context.Context; 039import org.apache.avalon.framework.context.ContextException; 040import org.apache.avalon.framework.context.Contextualizable; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.avalon.framework.service.Serviceable; 044 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.data.type.impl.PasswordRepositoryElementType; 047import org.ametys.cms.search.model.SystemProperty; 048import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 049import org.ametys.core.util.LambdaUtils; 050import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 051import org.ametys.runtime.parameter.Enumerator; 052import org.ametys.runtime.plugin.component.AbstractLogEnabled; 053import org.ametys.runtime.plugin.component.PluginAware; 054import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 055import org.ametys.web.repository.site.Site; 056import org.ametys.web.search.misc.SiteQueryHelper; 057import org.ametys.web.service.ServiceExtensionPoint; 058import org.ametys.web.service.ServiceParameter; 059import org.ametys.web.site.SiteEnumerator; 060 061/** 062 * Helper component for drawing search service creation/edition dialog box. 063 */ 064public class SearchServiceCreationHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable, Disposable, PluginAware, Contextualizable 065{ 066 /** Avalon Role */ 067 public static final String ROLE = SearchServiceCreationHelper.class.getName(); 068 069 /** The service manager */ 070 protected ServiceManager _manager; 071 /** The extension point for {@link Returnable}s */ 072 protected ReturnableExtensionPoint _returnableEP; 073 074 /** The extension point for {@link Searchable}s */ 075 protected SearchableExtensionPoint _searchableEP; 076 077 /** The extension point for services */ 078 protected ServiceExtensionPoint _serviceEP; 079 /** The extension point for content types */ 080 protected ContentTypeExtensionPoint _cTypeEP; 081 /** The extension point for {@link SystemProperty SystemProperties} */ 082 protected SystemPropertyExtensionPoint _systemPropertyEP; 083 /** The helper for site query */ 084 protected SiteQueryHelper _siteQueryHelper; 085 /** The enumerator manager */ 086 protected ThreadSafeComponentManager<Enumerator> _enumeratorManager; 087 /** The {@link Site} enumerator */ 088 protected SiteEnumerator _siteEnumerator; 089 090 /** The plugin name */ 091 protected String _pluginName; 092 093 /** The context */ 094 protected Context _context; 095 096 /** The map returnable -> Collection of searchables */ 097 protected Map<Returnable, Collection<Searchable>> _searchablesByReturnable; 098 099 private Collection<AdditionalSearchServiceParameter> _additionalSearchServiceParameters; 100 101 private boolean _initialized; 102 103 @Override 104 public void service(ServiceManager manager) throws ServiceException 105 { 106 _manager = manager; 107 _returnableEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE); 108 _searchableEP = (SearchableExtensionPoint) manager.lookup(SearchableExtensionPoint.ROLE); 109 _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 110 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 111 _systemPropertyEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 112 113 _siteQueryHelper = (SiteQueryHelper) manager.lookup(SiteQueryHelper.ROLE); 114 } 115 116 @Override 117 public void setPluginInfo(String pluginName, String featureName, String id) 118 { 119 _pluginName = pluginName; 120 } 121 122 @Override 123 public void contextualize(Context context) throws ContextException 124 { 125 _context = context; 126 } 127 128 @Override 129 public void initialize() throws Exception 130 { 131 buildTree(); 132 createEnumerators(); 133 _initialized = true; 134 } 135 136 @Override 137 public void dispose() 138 { 139 _initialized = false; 140 } 141 142 /** 143 * Is this creation helper ready ? 144 * @return <code>true</code> if this creation helper is ready 145 */ 146 public boolean isReady() 147 { 148 return _initialized; 149 } 150 151 /** 152 * Method called at the start of the application based on 153 * {@link Returnable#relationsWith()} and {@link Searchable#relationsWith()} 154 * in order to build the data structure (in a map returnable -> Collection of searchables) 155 * linking {@link Returnable}s and {@link Searchable}s 156 */ 157 protected void buildTree() 158 { 159 _searchablesByReturnable = new HashMap<>(); 160 for (String returnableId : _returnableEP.getExtensionsIds()) 161 { 162 Returnable returnable = _returnableEP.getExtension(returnableId); 163 Collection<Searchable> searchables = new HashSet<>(); 164 _searchablesByReturnable.put(returnable, searchables); 165 searchables.addAll(returnable.relationsWith()); 166 } 167 168 for (String searchableId : _searchableEP.getExtensionsIds()) 169 { 170 Searchable searchable = _searchableEP.getExtension(searchableId); 171 for (Returnable returnable : searchable.relationsWith()) 172 { 173 _searchablesByReturnable.get(returnable)/*cannot be null as we iterated over all resultType extensions*/.add(searchable); 174 } 175 } 176 177 _logTree(); 178 } 179 180 private void _logTree() 181 { 182 if (getLogger().isInfoEnabled()) 183 { 184 var sb = new StringBuilder("Searchables per returnable:"); 185 for (Entry<Returnable, Collection<Searchable>> e : _searchablesByReturnable.entrySet()) 186 { 187 sb.append("\n* ") 188 .append(e.getKey().getId()) 189 .append("=") 190 .append(e.getValue()/*unfortunately Searchable do not have getId, so keep a Collection<Searchable>*/); 191 } 192 getLogger().info(sb.toString()); 193 } 194 } 195 196 /** 197 * Creates and initializes enumerators ({@link SiteEnumerator}, etc.) 198 * @throws Exception if an error occurs 199 */ 200 protected void createEnumerators() throws Exception 201 { 202 _enumeratorManager = new ThreadSafeComponentManager<>(); 203 _enumeratorManager.setLogger(getLogger()); 204 _enumeratorManager.contextualize(_context); 205 _enumeratorManager.service(_manager); 206 207 final String siteRole = "site"; 208 _enumeratorManager.addComponent(_pluginName, null, siteRole, SiteEnumerator.class, new DefaultConfiguration("enumerator")); 209 _enumeratorManager.initialize(); 210 _siteEnumerator = (SiteEnumerator) _enumeratorManager.lookup(siteRole); 211 Objects.nonNull(_siteEnumerator); 212 } 213 214 /** 215 * Gets the {@link Returnable}s with the given ids 216 * @param returnableIds the ids of the {@link Returnable}s 217 * @return the {@link Returnable}s with the given ids 218 */ 219 public List<Returnable> getReturnables(List<String> returnableIds) 220 { 221 return returnableIds 222 .stream() 223 .map(_returnableEP::getExtension) 224 .filter(Objects::nonNull) 225 .collect(Collectors.toList()); 226 } 227 228 /** 229 * Gets the {@link Searchable}s linked with the given {@link Returnable}s 230 * @param returnables the {@link Returnable}s 231 * @return the {@link Searchable}s linked with the given {@link Returnable} 232 */ 233 public Collection<Searchable> getSearchables(Collection<Returnable> returnables) 234 { 235 return _getSearchables(returnables.stream()) 236 .collect(Collectors.toList()); 237 } 238 239 private Stream<Searchable> _getSearchables(Stream<Returnable> returnables) 240 { 241 return returnables 242 .map(_searchablesByReturnable::get) 243 .flatMap(Collection::stream) 244 .sorted(Comparator.comparingInt(Searchable::criteriaPosition)) 245 .distinct(); 246 } 247 248 /** 249 * Gets the {@link SearchCriterionDefinition}s available with the given {@link Searchable}s and additional parameter values 250 * @param searchables The {@link Searchable}s 251 * @param additionalParameterValues The additional parameter values 252 * @return the {@link SearchCriterionDefinition}s available with the given {@link Searchable}s and additional parameter values 253 */ 254 public Map<String, SearchCriterionDefinition> getCriterionDefinitions(Collection<Searchable> searchables, AdditionalParameterValueMap additionalParameterValues) 255 { 256 Map<String, SearchCriterionDefinition> criterionDefs = new LinkedHashMap<>(); 257 258 criterionDefs.putAll(SearchServiceCommonImpls.getCommonCriterionDefinitions(this)); 259 260 searchables.stream() 261 .map(searchable -> searchable.getCriteria(additionalParameterValues)) 262 .flatMap(Collection::stream) 263 .distinct() 264 .forEach(criterionDef -> criterionDefs.put(criterionDef.getId(), criterionDef)); 265 266 return criterionDefs; 267 } 268 269 /** 270 * Gets the {@link FacetDefinition}s available with the given {@link Returnable}s and additional parameter values 271 * @param returnables The {@link Returnable}s 272 * @param additionalParameterValues The additional parameter values 273 * @return the {@link FacetDefinition}s available with the given {@link Returnable}s and additional parameter values 274 */ 275 public Map<String, FacetDefinition> getFacetDefinitions(Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues) 276 { 277 Map<String, FacetDefinition> facetDefs = new LinkedHashMap<>(); 278 279 facetDefs.putAll(SearchServiceCommonImpls.getCommonFacetDefinitions(this)); 280 281 returnables.stream() 282 .map(returnable -> returnable.getFacets(additionalParameterValues)) 283 .flatMap(Collection::stream) 284 .distinct() 285 .forEach(facetDef -> facetDefs.put(facetDef.getId(), facetDef)); 286 287 return facetDefs; 288 } 289 290 /** 291 * Gets the {@link SortDefinition}s available with the given {@link Returnable}s and additional parameter values 292 * @param returnables The {@link Returnable}s 293 * @param additionalParameterValues The additional parameter values 294 * @return the {@link SortDefinition}s available with the given {@link Returnable}s and additional parameter values 295 */ 296 public Map<String, SortDefinition> getSortDefinitions(Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues) 297 { 298 Map<String, SortDefinition> sortDefs = new LinkedHashMap<>(); 299 300 sortDefs.putAll(SearchServiceCommonImpls.getCommonSortDefinitions(this)); 301 302 if (returnables.size() == 1) 303 // otherwise do not return them as it is not relevant to sort on a field not present on all returned documents 304 { 305 Returnable returnable = returnables.iterator().next(); 306 returnable.getSorts(additionalParameterValues) 307 .stream() 308 .forEach(sortDef -> sortDefs.put(sortDef.getId(), sortDef)); 309 } 310 311 return sortDefs; 312 } 313 314 /** 315 * Gets the {@link Returnable}s that must be selected by default 316 * @return the {@link Returnable}s that must be selected by default 317 */ 318 public Collection<String> selectedReturnables() 319 { 320 return _returnableEP.getExtensionsIds() 321 .stream() 322 .map(_returnableEP::getExtension) 323 .filter(Returnable::selectedByDefault) 324 .map(Returnable::getId) 325 .collect(Collectors.toList()); 326 } 327 328 /** 329 * Gets the configurations of the additional parameters to display in general group 330 * @return the configurations of the additional parameters to display 331 */ 332 public Collection<Configuration> getAdditionalParameterConfigurationsForGeneral() 333 { 334 return _getAdditionalParameterConfigurationsOfSearchables(); 335 } 336 337 private Collection<Configuration> _getAdditionalParameterConfigurationsOfSearchables() 338 { 339 Stream<Returnable> allReturnables = _returnableEP.getExtensionsIds() 340 .stream() 341 .map(_returnableEP::getExtension); 342 343 return _getSearchables(allReturnables) 344 // do not do _searchableEP.getExtensionIds() because we do not want the searchables not linked to any returnable 345 .map(LambdaUtils.wrap(searchable -> searchable.additionalServiceParameters())) 346 .flatMap(Collection::stream) 347 .collect(Collectors.toList()); 348 } 349 350 /** 351 * Gets the configurations of the additional parameters to display in display group 352 * @return the configurations of the additional parameters to display 353 */ 354 public Collection<Configuration> getAdditionalParameterConfigurationsForDisplay() 355 { 356 return _getAdditionalParameterConfigurationsOfReturnables(); 357 } 358 359 private Collection<Configuration> _getAdditionalParameterConfigurationsOfReturnables() 360 { 361 return _returnableEP.getExtensionsIds() 362 .stream() 363 .map(_returnableEP::getExtension) 364 .map(LambdaUtils.wrap(returnable -> returnable.additionalServiceParameters())) 365 .flatMap(Collection::stream) 366 .collect(Collectors.toList()); 367 } 368 369 /** 370 * Sets the additional parameters to display 371 * @param additionalSearchServiceParameters the additional parameters to display 372 */ 373 public void setAdditionalParameters(Collection<AdditionalSearchServiceParameter> additionalSearchServiceParameters) 374 { 375 Objects.requireNonNull(additionalSearchServiceParameters); 376 _additionalSearchServiceParameters = additionalSearchServiceParameters; 377 } 378 379 /** 380 * Gets the additional parameters to display 381 * @return the additional parameters to display 382 */ 383 public Collection<AdditionalSearchServiceParameter> getAdditionalParameters() 384 { 385 return Optional.ofNullable(_additionalSearchServiceParameters) 386 .orElseThrow(() -> new IllegalStateException("Too soon to call #getAdditionalParameters as they are not initialized yet. #setAdditionalParameters was not called.")); 387 } 388 389 /** 390 * Gets the values of the additional parameters 391 * @param additionalParameters the additional parameters 392 * @param serviceParameters the storage of the service parameters 393 * @return the values of the additional parameters 394 */ 395 public AdditionalParameterValueMap getAdditionalParameterValues(Collection<AdditionalSearchServiceParameter> additionalParameters, ModelAwareDataHolder serviceParameters) 396 { 397 Map<String, Object> additionalParameterValues = new HashMap<>(); 398 for (AdditionalSearchServiceParameter additionalParameter : additionalParameters) 399 { 400 ServiceParameter<Object> parameter = additionalParameter.getParameter(); 401 String paramId = parameter.getName(); 402 additionalParameterValues.put(paramId, _getAdditionalParameterValue(serviceParameters, parameter)); 403 } 404 405 Set<String> notDisplayableParameterIds = _notDisplayableParameterIds(additionalParameters); 406 407 return new AdditionalParameterValueMap(additionalParameterValues, notDisplayableParameterIds); 408 } 409 410 private <T> Object _getAdditionalParameterValue(ModelAwareDataHolder serviceParameters, ServiceParameter<T> parameter) 411 { 412 String parameterName = parameter.getName(); 413 if (serviceParameters.hasValue(parameterName)) 414 { 415 T value = serviceParameters.getValue(parameterName); 416 return serviceParameters.isMultiple(parameterName) ? Arrays.asList((Object[]) value) : value; 417 } 418 return null; 419 } 420 421 /** 422 * Gets the values of the additional parameters from a client form 423 * @param additionalParameters the additional parameters 424 * @param clientSideValues the values of the client form 425 * @return the values of the additional parameters from the client form 426 */ 427 public AdditionalParameterValueMap getAdditionalParameterValues(Collection<AdditionalSearchServiceParameter> additionalParameters, Map<String, Object> clientSideValues) 428 { 429 Set<String> paramIds = additionalParameters.stream() 430 .map(AdditionalSearchServiceParameter::getParameter) 431 .map(ServiceParameter::getName) 432 .collect(Collectors.toSet()); 433 434 Map<String, Object> additionalParameterValues = new HashMap<>(); 435 clientSideValues.keySet() 436 .stream() 437 .filter(paramIds::contains) 438 .forEach(id -> additionalParameterValues.put(id, clientSideValues.get(id))); // bug with Collectors.toMap see https://bugs.openjdk.java.net/browse/JDK-8148463 439 440 Set<String> notDisplayableParameterIds = _notDisplayableParameterIds(additionalParameters); 441 442 return new AdditionalParameterValueMap(additionalParameterValues, notDisplayableParameterIds); 443 } 444 445 private Set<String> _notDisplayableParameterIds(Collection<AdditionalSearchServiceParameter> additionalParameters) 446 { 447 return additionalParameters 448 .parallelStream() 449 .map(AdditionalSearchServiceParameter::getParameter) 450 .filter(p-> PasswordRepositoryElementType.TYPE_ID.equals(p.getType().getId())) 451 .map(ServiceParameter::getName) 452 .collect(Collectors.toSet()); 453 } 454}