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