001/* 002 * Copyright 2017 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.contenttype.validation; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.List; 021import java.util.Map; 022 023import org.apache.avalon.framework.configuration.Configurable; 024import org.apache.avalon.framework.configuration.Configuration; 025import org.apache.avalon.framework.configuration.ConfigurationException; 026import org.apache.commons.lang3.StringUtils; 027 028import org.ametys.cms.contenttype.AttributeDefinition; 029import org.ametys.cms.contenttype.ContentValidator; 030import org.ametys.cms.repository.Content; 031import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 032import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 033import org.ametys.plugins.repository.model.CompositeDefinition; 034import org.ametys.plugins.repository.model.RepeaterDefinition; 035import org.ametys.runtime.i18n.I18nizableText; 036import org.ametys.runtime.model.ElementDefinition; 037import org.ametys.runtime.model.ModelItem; 038import org.ametys.runtime.model.ModelItemGroup; 039import org.ametys.runtime.model.View; 040import org.ametys.runtime.model.type.ElementType; 041import org.ametys.runtime.parameter.Errors; 042 043/** 044 * Base {@link ContentValidator} validating the content by comparing one or more couples of values. 045 * For each couple of values, the identified max value should be greater than or equal to the identified min value. 046 * 047 * @param <T> The type tested by the validator. 048 */ 049public abstract class AbstractIntervalValidator<T> extends AbstractContentValidator implements Configurable 050{ 051 private String _minDefinitionPath; 052 private String _maxDefinitionPath; 053 private boolean _mandatory; 054 055 @Override 056 public void configure(Configuration configuration) throws ConfigurationException 057 { 058 _minDefinitionPath = configureMinPath(configuration); 059 _maxDefinitionPath = configureMaxPath(configuration); 060 _mandatory = configuration.getChild("mandatory", true).getValueAsBoolean(false); 061 } 062 063 @Override 064 public void validate(Content content, Errors errors) 065 { 066 ElementDefinition minDefinition = (ElementDefinition) content.getDefinition(_minDefinitionPath); 067 ElementDefinition maxDefinition = (ElementDefinition) content.getDefinition(_maxDefinitionPath); 068 069 _checkAttributeDefinitions(minDefinition, maxDefinition); 070 071 Object minValues = content.getValue(_minDefinitionPath, true); 072 Object maxValues = content.getValue(_maxDefinitionPath, true); 073 074 _validateInterval(content, errors, minDefinition, maxDefinition, minValues, maxValues); 075 } 076 077 @SuppressWarnings("unchecked") 078 private List<T> _getValuesAsList(Object value) 079 { 080 if (value == null) 081 { 082 List<T> result = new ArrayList<>(); 083 result.add(null); 084 return result; 085 } 086 087 return value instanceof List ? (List<T>) value : value.getClass().isArray() ? Arrays.asList((T[]) value) : List.of((T) value); 088 } 089 090 /** 091 * Validate the interval values of a content 092 * @param content The content 093 * @param errors The list of errors to fill 094 * @param minDefinition The definition for the min value 095 * @param maxDefinition The definition for the max value 096 * @param minValues The list of min values 097 * @param maxValues The list of max values 098 */ 099 protected void _validateInterval(Content content, Errors errors, ElementDefinition minDefinition, ElementDefinition maxDefinition, Object minValues, Object maxValues) 100 { 101 List<T> listMinValues = _getValuesAsList(minValues); 102 List<T> listMaxValues = _getValuesAsList(maxValues); 103 104 if (listMinValues.size() != listMaxValues.size()) 105 { 106 if (getLogger().isDebugEnabled()) 107 { 108 getLogger().debug("Could not process with the content validation: did not found a min/max couples for each values."); 109 } 110 111 errors.addError(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_VALIDATOR_NUMBER_INTERVAL_SIZEERROR")); 112 return; 113 } 114 115 for (int i = 0; i < listMinValues.size(); i++) 116 { 117 T min = listMinValues.get(i); 118 T max = listMaxValues.get(i); 119 120 if (_mandatory && (min != null ^ max != null)) 121 { 122 I18nizableText emptyLabel = min == null ? minDefinition.getLabel() : maxDefinition.getLabel(); 123 if (getLogger().isDebugEnabled()) 124 { 125 getLogger().debug("Content '" + content.getId() + "' is invalid : " + emptyLabel + " is empty but is part of a mandatory interval"); 126 } 127 128 addErrorEmpty(errors, minDefinition, maxDefinition, emptyLabel); 129 } 130 else if (min != null && max != null && isLessThan(max, min)) 131 { 132 if (getLogger().isDebugEnabled()) 133 { 134 getLogger().debug("Content '" + content.getId() + "' is invalid : " + min + " [" + _minDefinitionPath + "] is greater than " + max + " [" + _maxDefinitionPath + "]"); 135 } 136 137 addIntervalError(errors, minDefinition, maxDefinition, min, max); 138 } 139 } 140 } 141 142 @Override 143 public void validate(Content content, Map<String, Object> values, View view, Errors errors) 144 { 145 AttributeDefinition minDefinition = (AttributeDefinition) content.getDefinition(_minDefinitionPath); 146 AttributeDefinition maxDefinition = (AttributeDefinition) content.getDefinition(_maxDefinitionPath); 147 148 _checkAttributeDefinitions(minDefinition, maxDefinition); 149 150 Object minValues; 151 Object maxValues; 152 153 if (minDefinition.canWrite(content)) 154 { 155 minValues = _getValues(content, values, _minDefinitionPath, null); 156 } 157 else 158 { 159 minValues = content.getValue(_minDefinitionPath, true); 160 } 161 162 if (maxDefinition.canWrite(content)) 163 { 164 maxValues = _getValues(content, values, _maxDefinitionPath, null); 165 } 166 else 167 { 168 maxValues = content.getValue(_minDefinitionPath, true); 169 } 170 171 _validateInterval(content, errors, minDefinition, maxDefinition, minValues, maxValues); 172 } 173 174 private void _checkAttributeDefinitions (ElementDefinition minDefinition, ElementDefinition maxDefinition) 175 { 176 if (minDefinition == null) 177 { 178 throw new IllegalArgumentException("Unable to validate content: the path '" + _minDefinitionPath + "' refers to a non existing attribute for content type " + getContentType().getId()); 179 } 180 if (maxDefinition == null) 181 { 182 throw new IllegalArgumentException("Unable to validate content: the path '" + _maxDefinitionPath + "' refers to a non existing attribute for content type " + getContentType().getId()); 183 } 184 185 if (!isSupportedType(minDefinition.getType())) 186 { 187 throw new IllegalArgumentException("Unable to validate content: the attribute to path '" + _minDefinitionPath + "' (" + minDefinition.getType() + ") is not the expected type"); 188 } 189 190 if (!isSupportedType(maxDefinition.getType())) 191 { 192 throw new IllegalArgumentException("Unable to validate content: the attribute to path '" + _maxDefinitionPath + "' (" + maxDefinition.getType() + ") is not the expected type"); 193 } 194 } 195 196 @SuppressWarnings("unchecked") 197 private List<T> _getValues(Content content, Map<String, Object> values, String definitionPath, ModelItemGroup parentDefinition) 198 { 199 String[] pathSegments = StringUtils.split(definitionPath, ModelItem.ITEM_PATH_SEPARATOR); 200 201 if (pathSegments.length == 0) 202 { 203 return null; 204 } 205 206 String fieldName = pathSegments[0]; 207 208 ModelItem modelItem = parentDefinition == null ? content.getDefinition(fieldName) : parentDefinition.getChild(fieldName); 209 210 Object value = DataHolderHelper.getValueToValidate(values.get(fieldName)); 211 if (value == null) 212 { 213 return null; 214 } 215 216 if (pathSegments.length == 1) 217 { 218 if (!(modelItem instanceof ElementDefinition)) 219 { 220 throw new IllegalArgumentException("Last path segment should correspond to an ElementType"); 221 } 222 223 ElementDefinition definition = (ElementDefinition) modelItem; 224 225 if (isSupportedType(definition.getType())) 226 { 227 return value.getClass().isArray() ? Arrays.asList((T[]) value) : List.of((T) value); 228 } 229 else 230 { 231 // Should never happen (this was checked before) 232 throw new IllegalArgumentException("Attribute '" + fieldName + "' (" + definition.getType() + ") is not the expected type"); 233 } 234 } 235 else 236 { 237 String remainingPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 238 239 if (modelItem instanceof CompositeDefinition) 240 { 241 return _getValues(content, (Map<String, Object>) value, remainingPath, (CompositeDefinition) modelItem); 242 } 243 else if (modelItem instanceof RepeaterDefinition) 244 { 245 List<Map<String, Object>> entries = value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries(); 246 247 List<T> result = new ArrayList<>(); 248 249 for (int i = 0; i < entries.size(); i++) 250 { 251 List<T> entryValues = _getValues(content, entries.get(i), remainingPath, (RepeaterDefinition) modelItem); 252 if (entryValues != null) 253 { 254 result.addAll(entryValues); 255 } 256 } 257 258 return result; 259 } 260 else 261 { 262 throw new IllegalArgumentException("Inner path segments should correspond to an ModelItemGroup"); 263 } 264 } 265 } 266 267 /** 268 * Get the metadata min path from the configuration 269 * @param configuration The configuration 270 * @return The metadata min path 271 * @throws ConfigurationException If an error occurs 272 */ 273 protected abstract String configureMinPath(Configuration configuration) throws ConfigurationException; 274 275 /** 276 * Get the metadata max path from the configuration 277 * @param configuration The configuration 278 * @return The metadata max path 279 * @throws ConfigurationException If an error occurs 280 */ 281 protected abstract String configureMaxPath(Configuration configuration) throws ConfigurationException; 282 283 /** 284 * Test if the first number is less than the second one. 285 * @param n1 The first number to compare. 286 * @param n2 The second number to compare. 287 * @return true if the first number is less than the first, false otherwise. 288 */ 289 protected abstract boolean isLessThan(T n1, T n2); 290 291 /** 292 * Test if the attribute type is one expected by the validator 293 * @param type The attribute type 294 * @return True if the type is supported 295 */ 296 protected abstract boolean isSupportedType(ElementType type); 297 298 299 /** 300 * Add an error when the max value is less than the min value 301 * @param errors The list of errors 302 * @param minDefinition The min definition 303 * @param maxDefinition The max definition 304 * @param min The min value 305 * @param max The max value 306 */ 307 protected abstract void addIntervalError(Errors errors, ElementDefinition minDefinition, ElementDefinition maxDefinition, T min, T max); 308 309 /** 310 * Add an error when the max or the min value is empty but the interval is mandatory 311 * @param errors The list of errors 312 * @param minDefinition The min definition 313 * @param maxDefinition The max definition 314 * @param emptyLabel The label of the empty field 315 */ 316 protected abstract void addErrorEmpty(Errors errors, ElementDefinition minDefinition, ElementDefinition maxDefinition, I18nizableText emptyLabel); 317}