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.addChild(initialConfiguration.getChild("label")); 138 finalConfiguration.addChild(initialConfiguration.getChild("criteriaPosition")); 139 finalConfiguration.addChild(buildParametersConfiguration(initialConfiguration)); 140 return finalConfiguration; 141 } 142 143 /** 144 * Builds the configuration object for parameters 145 * @param initialConfiguration The source configuration 146 * @return the configuration object for parameters 147 * @throws ConfigurationException If some values or attributes cannot be retrieved 148 */ 149 protected Configuration buildParametersConfiguration(Configuration initialConfiguration) throws ConfigurationException 150 { 151 Configuration sourceParameter = initialConfiguration.getChild("contentTypeParameter"); 152 153 DefaultConfiguration parameters = new DefaultConfiguration("parameters"); 154 DefaultConfiguration targetParameter = new DefaultConfiguration("parameter"); 155 parameters.addChild(targetParameter); 156 targetParameter.setAttribute("name", _parameterSubContentTypeUniqueName); 157 targetParameter.setAttribute("type", "string"); 158 targetParameter.setAttribute("multiple", "true"); 159 targetParameter.setAttribute("reloadCriteriaOnChange", "true"); 160 if (sourceParameter.getAttribute("order", null) != null) 161 { 162 targetParameter.setAttribute("order", sourceParameter.getAttribute("order")); 163 } 164 165 targetParameter.addChild(sourceParameter.getChild("label")); 166 targetParameter.addChild(sourceParameter.getChild("description")); 167 168 DefaultConfiguration disableConditions = new DefaultConfiguration("disable-conditions"); 169 targetParameter.addChild(disableConditions); 170 disableConditions.setAttribute("type", "and"); 171 DefaultConfiguration disableCondition = new DefaultConfiguration("condition"); 172 disableConditions.addChild(disableCondition); 173 disableCondition.setAttribute("id", "returnables"); 174 disableCondition.setAttribute("operator", "neq"); 175 disableCondition.setValue(_associatedContentReturnableRole); 176 177 DefaultConfiguration widget = new DefaultConfiguration("widget"); 178 targetParameter.addChild(widget); 179 widget.setValue("edition.select-content-types"); 180 181 DefaultConfiguration widgetParams = new DefaultConfiguration("widget-params"); 182 targetParameter.addChild(widgetParams); 183 DefaultConfiguration emptyTextParam = new DefaultConfiguration("param"); 184 widgetParams.addChild(emptyTextParam); 185 emptyTextParam.setAttribute("name", "emptyText"); 186 emptyTextParam.setAttribute("i18n", "true"); 187 emptyTextParam.setValue("plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS"); 188 189 DefaultConfiguration enumeration = new DefaultConfiguration("enumeration"); 190 targetParameter.addChild(enumeration); 191 DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator"); 192 enumeration.addChild(customEnumerator); 193 customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName()); 194 DefaultConfiguration excludePrivate = new DefaultConfiguration("excludePrivate"); 195 customEnumerator.addChild(excludePrivate); 196 excludePrivate.setValue("false"); 197 DefaultConfiguration contentTypeConf = new DefaultConfiguration("contentTypes"); 198 customEnumerator.addChild(contentTypeConf); 199 contentTypeConf.setValue(String.join(",", _baseContentTypes)); 200 201 return parameters; 202 } 203 204 @Override 205 public void initialize() throws Exception 206 { 207 _initCriterionDefinitionPrefix(); 208 super.initialize(); 209 } 210 211 @Override 212 protected String associatedContentReturnableRole() 213 { 214 return _associatedContentReturnableRole; 215 } 216 217 @Override 218 protected String getCriterionDefinitionPrefix() 219 { 220 return _criterionDefinitionPrefix; 221 } 222 223 @Override 224 public Collection<Returnable> relationsWith() 225 { 226 return Collections.singleton(_associatedContentReturnable); 227 } 228 229 @Override 230 protected Collection<String> getContentTypes(AdditionalParameterValueMap additionalParameterValues) 231 { 232 Collection<String> contentTypes = additionalParameterValues.getValue(_parameterSubContentTypeUniqueName); 233 return CollectionUtils.isNotEmpty(contentTypes) 234 ? contentTypes 235 : _baseContentTypes; 236 } 237 238 @Override 239 public Optional<Query> joinQuery(Query queryOnCriterion, SearchCriterionDefinition criterion, Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameters) 240 { 241 if (returnables.contains(_associatedContentReturnable)) 242 { 243 return Optional.of(queryOnCriterion); 244 } 245 else 246 { 247 return Optional.empty(); 248 } 249 } 250 251 /** 252 * Initializes {@link #_avalonRole} field from configuration 253 * @param configuration The configuration 254 * @throws ConfigurationException If a configuration value cannot be retrieved 255 */ 256 protected void _initAvalonRole(Configuration configuration) throws ConfigurationException 257 { 258 _avalonRole = configuration.getAttribute("id"); 259 } 260 261 /** 262 * Initializes {@link #_shortAvalonRole} field 263 */ 264 protected void _initShortAvalonRole() 265 { 266 // Pray for shortId to be unique. If not, method visibilities are protected for implementing you own logic. 267 _shortAvalonRole = StringUtils.substringAfterLast(_avalonRole, "."); 268 } 269 270 /** 271 * Initializes {@link #_parameterSubContentTypeUniqueName} field 272 */ 273 protected void _initParameterSubContentTypeUniqueName() 274 { 275 _parameterSubContentTypeUniqueName = _shortAvalonRole + __PARAMETER_SUB_CONTENT_TYPES_SUFFIX; 276 } 277 278 /** 279 * Initializes {@link #_associatedContentReturnableRole} field from configuration 280 * @param configuration The configuration 281 * @throws ConfigurationException If a configuration value cannot be retrieved 282 */ 283 protected void _configureAssociatedContentReturnableRole(Configuration configuration) throws ConfigurationException 284 { 285 _associatedContentReturnableRole = configuration.getChild("associatedReturnable").getValue(); 286 } 287 288 /** 289 * Initializes {@link #_baseContentTypes} field from configuration 290 * @param configuration The configuration 291 * @throws ConfigurationException If a configuration value cannot be retrieved 292 */ 293 protected void _configureBaseContentTypes(Configuration configuration) throws ConfigurationException 294 { 295 Configuration[] baseContentTypeConfs = configuration.getChildren("baseContentType"); 296 _baseContentTypes = Stream.of(baseContentTypeConfs) 297 .map(LambdaUtils.wrap(Configuration::getValue)) 298 .peek(cTypeId -> 299 { 300 if (!_isHandledContentType(cTypeId)) 301 { 302 getLogger().warn("The baseContentType '{}' for '{}' does not exist or is not private", cTypeId, _avalonRole); 303 } 304 }) 305 .filter(this::_isHandledContentType) 306 .collect(Collectors.toList()); 307 308 if (_baseContentTypes.isEmpty()) 309 { 310 throw new IllegalArgumentException("The baseContentType configurations are missing or are not valid."); 311 } 312 } 313 314 private boolean _isHandledContentType(String cTypeId) 315 { 316 return _cTypeEP.hasExtension(cTypeId) && _cTypeEP.getExtension(cTypeId).isPrivate(); 317 } 318 319 /** 320 * Initializes {@link #_criterionDefinitionPrefix} field 321 */ 322 protected void _initCriterionDefinitionPrefix() 323 { 324 _criterionDefinitionPrefix = _shortAvalonRole + "$"; 325 } 326}