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