001/* 002 * Copyright 2019 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.impl; 017 018import java.util.Collection; 019import java.util.Collections; 020import java.util.HashSet; 021import java.util.Optional; 022import java.util.Set; 023import java.util.stream.Collectors; 024import java.util.stream.Stream; 025 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.configuration.DefaultConfiguration; 029import org.apache.commons.collections4.CollectionUtils; 030import org.apache.commons.lang3.StringUtils; 031 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeEnumerator; 034import org.ametys.cms.search.query.Query; 035import org.ametys.core.util.LambdaUtils; 036import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap; 037import org.ametys.web.frontoffice.search.metamodel.Returnable; 038import org.ametys.web.frontoffice.search.metamodel.SearchServiceCriterionDefinition; 039 040/** 041 * This class is a generic searchable to be used to search on a {@link ContentType#isPrivate() private} {@link ContentType}. 042 * <br>You <b>must</b> associate it with a {@link Returnable} which is or extends {@link PrivateContentReturnable} 043 * <pre> 044 * <extension point="org.ametys.web.frontoffice.search.metamodel.SearchableExtensionPoint" 045 * id="my.searchable.id" 046 * class="org.ametys.web.frontoffice.search.metamodel.impl.PrivateContentSearchable"> 047 * <label i18n="true">...</label> 048 * <criteriaPosition>11</criteriaPosition> 049 * <associatedReturnable>my.returnable.id</associatedReturnable> 050 * <baseContentType>my.content.type.1</baseContentType> 051 * <baseContentType>my.content.type.2</baseContentType> 052 * <contentTypeParameter order="99"> 053 * <label i18n="true">...</label> 054 * <description i18n="true">...</description> 055 * </contentTypeParameter> 056 * </extension> 057 * </pre> 058 */ 059public class PrivateContentSearchable extends AbstractContentBasedSearchable 060{ 061 private static final String __PARAMETER_SUB_CONTENT_TYPES_SUFFIX = "-subContentTypes"; 062 063 /** The Avalon role of this Searchable */ 064 protected String _avalonRole; 065 /** The "short version" of the Avalon role of this Searchable */ 066 protected String _shortAvalonRole; 067 /** The unique name accross the whole application for the parameter for this searchable to select the sub content types. */ 068 protected String _parameterSubContentTypeUniqueName; 069 /** The associated content returnable role */ 070 protected String _associatedContentReturnableRole; 071 /** The {@link #getCriterionDefinitionPrefix() prefix} for criterion definitions */ 072 protected String _criterionDefinitionPrefix; 073 /** The base private content types of this searchable */ 074 protected Set<String> _baseContentTypes; 075 076 @Override 077 public void configure(Configuration configuration) throws ConfigurationException 078 { 079 _initAvalonRole(configuration); 080 _initShortAvalonRole(); 081 _initParameterSubContentTypeUniqueName(); 082 _configureAssociatedContentReturnableRole(configuration); 083 _configureBaseContentTypes(configuration); 084 Configuration finalConfiguration = buildFinalConfiguration(configuration); 085 super.configure(finalConfiguration); 086 } 087 088 /** 089 * Builds the final configuration object which will be passed to the superclass, from the "real" XML configuration 090 * @param initialConfiguration The source configuration 091 * @return the built configuration object 092 * @throws ConfigurationException If some values or attributes cannot be retrieved 093 */ 094 protected Configuration buildFinalConfiguration(Configuration initialConfiguration) throws ConfigurationException 095 { 096 /* 097 * From the input configuration: 098 * 099 * <extension point="org.ametys.web.frontoffice.search.metamodel.SearchableExtensionPoint" 100 * id="{org.ametys...MyCustomSearchable}" 101 * class="org.ametys.web.frontoffice.search.metamodel.impl.PrivateContentSearchable"> 102 * <label i18n="true">...</label> 103 * <criteriaPosition>...</criteriaPosition> 104 * <associatedReturnable>{org.ametys...MyCustomAssociatedReturnable}</associatedReturnable> 105 * <baseContentType>my.content.type.1</baseContentType> 106 * <baseContentType>my.content.type.2</baseContentType> 107 * <contentTypeParameter order="10"> 108 * <label i18n="true">...</label> 109 * <description i18n="true">...</description> 110 * </contentTypeParameter> 111 * </extension> 112 * 113 * generates an automatic configuration: 114 * 115 * <extension> 116 * <label i18n="true">...</label> 117 * <criteriaPosition>...</criteriaPosition> 118 * <parameters> 119 * <parameter name="{MyCustomSearchable}$subContentTypes" type="string" multiple="true" reloadCriteriaOnChange="true" order="10"> 120 * <label i18n="true">...</label> 121 * <disable-conditions type="and"> 122 * <condition id="returnables" operator="neq">{org.ametys...MyCustomAssociatedReturnable}</condition> 123 * </disable-conditions> 124 * <widget>edition.select-content-types</widget> 125 * <widget-params> 126 * <param name="emptyText" i18n="true">plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS</param> 127 * </widget-params> 128 * <enumeration> 129 * <custom-enumerator class="org.ametys.cms.contenttype.ContentTypeEnumerator"> 130 * <excludePrivate>false</excludePrivate> 131 * <contentTypes>my.content.type.1,my.content.type.2</contentTypes> 132 * </custom-enumerator> 133 * </enumeration> 134 * </parameter> 135 * </parameters> 136 * </extension> 137 */ 138 DefaultConfiguration finalConfiguration = new DefaultConfiguration("extension"); 139 finalConfiguration.setAttribute("id", initialConfiguration.getAttribute("id")); 140 finalConfiguration.addChild(initialConfiguration.getChild("label")); 141 finalConfiguration.addChild(initialConfiguration.getChild("criteriaPosition")); 142 finalConfiguration.addChild(buildParametersConfiguration(initialConfiguration)); 143 return finalConfiguration; 144 } 145 146 /** 147 * Builds the configuration object for parameters 148 * @param initialConfiguration The source configuration 149 * @return the configuration object for parameters 150 * @throws ConfigurationException If some values or attributes cannot be retrieved 151 */ 152 protected Configuration buildParametersConfiguration(Configuration initialConfiguration) throws ConfigurationException 153 { 154 Configuration sourceParameter = initialConfiguration.getChild("contentTypeParameter"); 155 156 DefaultConfiguration parameters = new DefaultConfiguration("parameters"); 157 DefaultConfiguration targetParameter = new DefaultConfiguration("parameter"); 158 parameters.addChild(targetParameter); 159 targetParameter.setAttribute("name", _parameterSubContentTypeUniqueName); 160 targetParameter.setAttribute("type", "string"); 161 targetParameter.setAttribute("multiple", "true"); 162 targetParameter.setAttribute("reloadCriteriaOnChange", "true"); 163 if (sourceParameter.getAttribute("order", null) != null) 164 { 165 targetParameter.setAttribute("order", sourceParameter.getAttribute("order")); 166 } 167 168 targetParameter.addChild(sourceParameter.getChild("label")); 169 targetParameter.addChild(sourceParameter.getChild("description")); 170 171 DefaultConfiguration disableConditions = new DefaultConfiguration("disable-conditions"); 172 targetParameter.addChild(disableConditions); 173 disableConditions.setAttribute("type", "and"); 174 DefaultConfiguration disableCondition = new DefaultConfiguration("condition"); 175 disableConditions.addChild(disableCondition); 176 disableCondition.setAttribute("id", "returnables"); 177 disableCondition.setAttribute("operator", "neq"); 178 disableCondition.setValue(_associatedContentReturnableRole); 179 180 DefaultConfiguration widget = new DefaultConfiguration("widget"); 181 targetParameter.addChild(widget); 182 widget.setValue("edition.select-content-types"); 183 184 DefaultConfiguration widgetParams = new DefaultConfiguration("widget-params"); 185 targetParameter.addChild(widgetParams); 186 DefaultConfiguration emptyTextParam = new DefaultConfiguration("param"); 187 widgetParams.addChild(emptyTextParam); 188 emptyTextParam.setAttribute("name", "emptyText"); 189 emptyTextParam.setAttribute("i18n", "true"); 190 emptyTextParam.setValue("plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS"); 191 192 DefaultConfiguration enumeration = new DefaultConfiguration("enumeration"); 193 targetParameter.addChild(enumeration); 194 DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator"); 195 enumeration.addChild(customEnumerator); 196 customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName()); 197 DefaultConfiguration excludePrivate = new DefaultConfiguration("excludePrivate"); 198 customEnumerator.addChild(excludePrivate); 199 excludePrivate.setValue("false"); 200 DefaultConfiguration contentTypeConf = new DefaultConfiguration("contentTypes"); 201 customEnumerator.addChild(contentTypeConf); 202 contentTypeConf.setValue(String.join(",", _baseContentTypes)); 203 204 return parameters; 205 } 206 207 @Override 208 public void initialize() throws Exception 209 { 210 _initCriterionDefinitionPrefix(); 211 super.initialize(); 212 } 213 214 @Override 215 protected String associatedContentReturnableRole() 216 { 217 return _associatedContentReturnableRole; 218 } 219 220 @Override 221 protected String getCriterionDefinitionPrefix() 222 { 223 return _criterionDefinitionPrefix; 224 } 225 226 @Override 227 public Collection<Returnable> relationsWith() 228 { 229 return Collections.singleton(_associatedContentReturnable); 230 } 231 232 @Override 233 protected Set<String> getContentTypeIds(AdditionalParameterValueMap additionalParameterValues) 234 { 235 Collection<String> contentTypeIds = additionalParameterValues.getValue(_parameterSubContentTypeUniqueName); 236 return CollectionUtils.isNotEmpty(contentTypeIds) 237 ? new HashSet<>(contentTypeIds) 238 : _baseContentTypes; 239 } 240 241 @Override 242 public Optional<Query> joinQuery(Query queryOnCriterion, SearchServiceCriterionDefinition criterion, Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameters) 243 { 244 if (returnables.contains(_associatedContentReturnable)) 245 { 246 return Optional.of(queryOnCriterion); 247 } 248 else 249 { 250 return Optional.empty(); 251 } 252 } 253 254 /** 255 * Initializes {@link #_avalonRole} field from configuration 256 * @param configuration The configuration 257 * @throws ConfigurationException If a configuration value cannot be retrieved 258 */ 259 protected void _initAvalonRole(Configuration configuration) throws ConfigurationException 260 { 261 _avalonRole = configuration.getAttribute("id"); 262 } 263 264 /** 265 * Initializes {@link #_shortAvalonRole} field 266 */ 267 protected void _initShortAvalonRole() 268 { 269 // Pray for shortId to be unique. If not, method visibilities are protected for implementing you own logic. 270 _shortAvalonRole = StringUtils.substringAfterLast(_avalonRole, "."); 271 } 272 273 /** 274 * Initializes {@link #_parameterSubContentTypeUniqueName} field 275 */ 276 protected void _initParameterSubContentTypeUniqueName() 277 { 278 _parameterSubContentTypeUniqueName = _shortAvalonRole + __PARAMETER_SUB_CONTENT_TYPES_SUFFIX; 279 } 280 281 /** 282 * Initializes {@link #_associatedContentReturnableRole} field from configuration 283 * @param configuration The configuration 284 * @throws ConfigurationException If a configuration value cannot be retrieved 285 */ 286 protected void _configureAssociatedContentReturnableRole(Configuration configuration) throws ConfigurationException 287 { 288 _associatedContentReturnableRole = configuration.getChild("associatedReturnable").getValue(); 289 } 290 291 /** 292 * Initializes {@link #_baseContentTypes} field from configuration 293 * @param configuration The configuration 294 * @throws ConfigurationException If a configuration value cannot be retrieved 295 */ 296 protected void _configureBaseContentTypes(Configuration configuration) throws ConfigurationException 297 { 298 Configuration[] baseContentTypeConfs = configuration.getChildren("baseContentType"); 299 _baseContentTypes = Stream.of(baseContentTypeConfs) 300 .map(LambdaUtils.wrap(Configuration::getValue)) 301 .peek(cTypeId -> 302 { 303 if (!_isHandledContentType(cTypeId)) 304 { 305 getLogger().warn("The baseContentType '{}' for '{}' does not exist or is not private", cTypeId, _avalonRole); 306 } 307 }) 308 .filter(this::_isHandledContentType) 309 .collect(Collectors.toSet()); 310 311 if (_baseContentTypes.isEmpty()) 312 { 313 throw new IllegalArgumentException("The baseContentType configurations are missing or are not valid."); 314 } 315 } 316 317 private boolean _isHandledContentType(String cTypeId) 318 { 319 return _contentTypeExtensionPoint.hasExtension(cTypeId) && _contentTypeExtensionPoint.getExtension(cTypeId).isPrivate(); 320 } 321 322 /** 323 * Initializes {@link #_criterionDefinitionPrefix} field 324 */ 325 protected void _initCriterionDefinitionPrefix() 326 { 327 _criterionDefinitionPrefix = _shortAvalonRole + "$"; 328 } 329}