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