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