001/* 002 * Copyright 2024 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.cms.search.model.impl; 017 018import java.util.HashSet; 019import java.util.Optional; 020import java.util.Set; 021 022import org.apache.avalon.framework.activity.Disposable; 023import org.apache.avalon.framework.component.Component; 024import org.apache.avalon.framework.configuration.Configurable; 025import org.apache.avalon.framework.configuration.Configuration; 026import org.apache.avalon.framework.configuration.ConfigurationException; 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.contenttype.ContentType; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.data.type.ModelItemTypeExtensionPoint; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.search.content.ContentSearchHelper; 041import org.ametys.cms.search.model.CriterionDefinitionHelper; 042import org.ametys.cms.search.model.SearchModelCriterionDefinition; 043import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 044import org.ametys.cms.search.query.Query.LogicalOperator; 045import org.ametys.cms.search.query.Query.Operator; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.model.ElementDefinition; 048import org.ametys.runtime.model.ItemParserHelper; 049import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName; 050import org.ametys.runtime.parameter.DefaultValidator; 051import org.ametys.runtime.parameter.Validator; 052import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 053import org.ametys.runtime.model.ModelHelper; 054import org.ametys.runtime.model.ModelItem; 055 056/** 057 * Static implementation for {@link SearchModelCriterionDefinition} searching on a model item. 058 * @param <T> Type of the criterion value 059 */ 060public class StaticReferencingSearchModelCriterionDefinition<T> extends ReferencingSearchModelCriterionDefinition<T> implements Component, Contextualizable, Serviceable, Configurable, Disposable 061{ 062 /** The system property extension point */ 063 protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 064 065 /** The content types helper */ 066 protected ContentTypesHelper _contentTypesHelper; 067 068 /** The criterion root configuration */ 069 protected Configuration _rootConfiguration; 070 071 /** ComponentManager for {@link Validator}s. */ 072 protected ThreadSafeComponentManager<Validator> _validatorManager; 073 074 public void contextualize(Context context) throws ContextException 075 { 076 __context = context; 077 } 078 079 public void service(ServiceManager manager) throws ServiceException 080 { 081 __serviceManager = manager; 082 083 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 084 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 085 086 _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE); 087 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 088 _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION); 089 _contentSearchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 090 } 091 092 @Override 093 public void configure(Configuration configuration) throws ConfigurationException 094 { 095 _rootConfiguration = configuration; 096 097 // First parse reference to have type of the criterion, used to parse some other data (default value, widget, ...) 098 setReferencePath(_parseReferencePath(configuration)); 099 setReference(_parseReference(configuration)); 100 101 ConfigurationAndPluginName configurationAndPluginName = new ConfigurationAndPluginName(configuration, getPluginName()); 102 setLabel(ItemParserHelper.parseI18nizableText(configurationAndPluginName, "label", (I18nizableText) null)); 103 setDescription(ItemParserHelper.parseI18nizableText(configurationAndPluginName, "description", (I18nizableText) null)); 104 105 setMultiple(ItemParserHelper.parseMultiple(configuration)); 106 setParsedDefaultValues(ItemParserHelper.parseDefaultValues(configuration, this, _criterionDefinitionHelper::parseCriterionDefinitionDefaultValue)); 107 108 setOperator(_parseOperator(configuration)); 109 setMultipleOperandOperator(LogicalOperator.valueOf(configuration.getChild("multiple-operand").getValue("or").toUpperCase())); 110 111 setWidget(ItemParserHelper.parseWidget(configuration)); 112 setWidgetParameters(ItemParserHelper.parseWidgetParameters(configuration, getPluginName())); 113 114 _validatorManager = new ThreadSafeComponentManager<>(); 115 _validatorManager.setLogger(_logger); 116 _validatorManager.contextualize(__context); 117 _validatorManager.service(__serviceManager); 118 119 Optional<String> validatorRole = _parseValidator(_validatorManager, configuration); 120 if (validatorRole.isPresent()) 121 { 122 try 123 { 124 _validatorManager.initialize(); 125 setValidator(_validatorManager.lookup(validatorRole.get())); 126 } 127 catch (Exception e) 128 { 129 throw new ConfigurationException("Unable to initialize the validator for criterion referencing '" + getReferencePath() + "'.", configuration, e); 130 } 131 } 132 } 133 134 /** 135 * Parse the criterion reference path. 136 * @param configuration The criterion definition configuration. 137 * @return The parsed reference path 138 * @throws ConfigurationException If an error occurs. 139 */ 140 protected String _parseReferencePath(Configuration configuration) throws ConfigurationException 141 { 142 String referencePath = configuration.getAttribute("ref", configuration.getChild("item").getAttribute("ref", null)); 143 144 if (referencePath == null) 145 { 146 throw new ConfigurationException("Unable to parse the reference of the criterion definition. A referencing criterion definition should specify its reference in an attribute 'ref'", configuration); 147 } 148 149 return referencePath; 150 } 151 152 /** 153 * Parse the criterion reference 154 * @param configuration the criterion definition configuration 155 * @return the definition of the reference 156 * @throws ConfigurationException If an error occurs 157 */ 158 @SuppressWarnings("unchecked") 159 protected ElementDefinition<T> _parseReference(Configuration configuration) throws ConfigurationException 160 { 161 String referencePath = getReferencePath(); 162 String[] referencePathSegments = StringUtils.split(referencePath, ModelItem.ITEM_PATH_SEPARATOR); 163 String lastReferencePathSegment = referencePathSegments[referencePathSegments.length - 1]; 164 165 if (_systemPropertyExtensionPoint.hasExtension(lastReferencePathSegment)) 166 { 167 return _systemPropertyExtensionPoint.getExtension(lastReferencePathSegment); 168 } 169 170 Set<ContentType> contentTypes = new HashSet<>(); 171 for (Configuration contentTypeConf : configuration.getChild("contentTypes").getChildren("type")) 172 { 173 String contentTypeId = contentTypeConf.getAttribute("id"); 174 if (_contentTypeExtensionPoint.hasExtension(contentTypeId)) 175 { 176 contentTypes.add(_contentTypeExtensionPoint.getExtension(contentTypeId)); 177 } 178 else 179 { 180 throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. The referenced content type '" + contentTypeId + "' does not exist", configuration); 181 } 182 } 183 184 if (contentTypes.isEmpty()) 185 { 186 if (Content.ATTRIBUTE_TITLE.equals(referencePath)) 187 { 188 return (ElementDefinition<T>) _contentTypesHelper.getTitleAttributeDefinition(); 189 } 190 else 191 { 192 throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. There is no provided content type, only title attribute can be referenced", configuration); 193 } 194 } 195 else 196 { 197 try 198 { 199 ModelItem modelItem = ModelHelper.getModelItem(referencePath, contentTypes); 200 if (modelItem instanceof ElementDefinition definition) 201 { 202 return definition; 203 } 204 else 205 { 206 throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. The path does not references an element definition but a group", configuration); 207 } 208 } 209 catch (Exception e) 210 { 211 throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. There is no model item at this path", configuration, e); 212 } 213 } 214 } 215 216 /** 217 * Parse the criterion operator 218 * @param configuration the criterion definition configuration 219 * @return the operator 220 * @throws ConfigurationException If an error occurs 221 */ 222 protected Operator _parseOperator(Configuration configuration) throws ConfigurationException 223 { 224 String operatorName = configuration.getChild("test-operator") 225 .getValue(null); 226 return operatorName != null ? Operator.fromName(operatorName) : null; 227 } 228 229 /** 230 * Parse the criterion validator. 231 * @param validatorManager The validator manager. 232 * @param config The validator configuration. 233 * @return The optional role of the validator, or an empty optional if there is no validator 234 * @throws ConfigurationException If an error occurs. 235 */ 236 @SuppressWarnings("unchecked") 237 protected Optional<String> _parseValidator(ThreadSafeComponentManager<Validator> validatorManager, Configuration config) throws ConfigurationException 238 { 239 Configuration validatorConfig = config.getChild("validation", false); 240 241 if (validatorConfig != null) 242 { 243 String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", StringUtils.EMPTY), DefaultValidator.class.getName()); 244 245 try 246 { 247 String validatorRole = "validator"; 248 Class validatorClass = Class.forName(validatorClassName); 249 validatorManager.addComponent(getPluginName(), null, validatorRole, validatorClass, config); 250 return Optional.of(validatorRole); 251 } 252 catch (Exception e) 253 { 254 throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e); 255 } 256 } 257 258 return Optional.empty(); 259 } 260 261 public void dispose() 262 { 263 _validatorManager.dispose(); 264 _validatorManager = null; 265 } 266 267 @Override 268 protected Configuration _getRootCriterionConfiguration() 269 { 270 return _rootConfiguration; 271 } 272}