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