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.odf.validator;
017
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.Set;
024
025import org.apache.avalon.framework.configuration.Configurable;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.StringUtils;
032
033import org.ametys.cms.contenttype.validation.AbstractContentValidator;
034import org.ametys.cms.data.holder.DataHolderDisableConditionsEvaluator;
035import org.ametys.cms.repository.Content;
036import org.ametys.odf.ODFHelper;
037import org.ametys.odf.data.EducationalPath;
038import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
039import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
040import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
041import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
042import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
043import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.runtime.i18n.I18nizableTextParameter;
046import org.ametys.runtime.model.ModelItem;
047import org.ametys.runtime.model.View;
048import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator;
049import org.ametys.runtime.parameter.ValidationResult;
050
051/**
052 * This global validation checks if the values of educational path attributs of a repeater entries are unique.<br>
053 * &lt;repeaterPath&gt;, &lt;educationalPathDataName&gt; and &lt;errorI18nKey&gt; configuration are expected.
054 */
055public class RepeaterWithEducationalPathValidator extends AbstractContentValidator implements Serviceable, Configurable
056{
057    private DisableConditionsEvaluator<ModelAwareDataHolder> _disableConditionsEvaluator;
058    private String _repeaterPath;
059    private String _educationalPathAttribute;
060    private ODFHelper _odfHelper;
061    private String _errorI18nKey;
062    
063    @SuppressWarnings("unchecked")
064    public void service(ServiceManager manager) throws ServiceException
065    {
066        _disableConditionsEvaluator = (DisableConditionsEvaluator<ModelAwareDataHolder>) manager.lookup(DataHolderDisableConditionsEvaluator.ROLE);
067        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
068    }
069    
070    public void configure(Configuration configuration) throws ConfigurationException
071    {
072        _repeaterPath = configuration.getChild("repeaterPath").getValue();
073        _educationalPathAttribute = configuration.getChild("educationalPathAttribute").getValue();
074        _errorI18nKey = configuration.getChild("errorI18nKey").getValue();
075    }
076    
077    @Override
078    public ValidationResult validate(Content content)
079    {
080        ValidationResult result = new ValidationResult();
081        
082        ModelItem repeaterDefinition = content.getDefinition(_repeaterPath);
083        
084        if (content.hasValue(_repeaterPath) && !_disableConditionsEvaluator.evaluateDisableConditions(repeaterDefinition, _repeaterPath, content))
085        {
086            Set<EducationalPath> paths = new HashSet<>();
087            
088            ModelAwareRepeater repeater = content.getRepeater(_repeaterPath);
089            
090            ModelItem contentDataDefinition = content.getDefinition(_repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute);
091            
092            for (ModelAwareRepeaterEntry entry : repeater.getEntries())
093            {
094                String dataPath = _repeaterPath + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute;
095                if (!_disableConditionsEvaluator.evaluateDisableConditions(contentDataDefinition, dataPath, content))
096                {
097                    EducationalPath path = entry.getValue(_educationalPathAttribute);
098                    
099                    if (path != null && !paths.add(path))
100                    {
101                        // Two entries or more have the same value for education path
102                        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
103                        i18nParams.put("path", new I18nizableText(_odfHelper.getEducationalPathAsString(path)));
104                        result.addError(new I18nizableText(StringUtils.substringBefore(_errorI18nKey, ':'), StringUtils.substringAfter(_errorI18nKey, ':'), i18nParams));
105                        break; // stop iteration
106                    }
107                }
108            }
109        }
110        
111        return result;
112    }
113    
114    @Override
115    public ValidationResult validate(Content content, Map<String, Object> values, View view)
116    {
117        ValidationResult result = new ValidationResult();
118        
119        Object repeater = values.get(_repeaterPath);
120        
121        List<Map<String, Object>> repeaterEntries =  repeater instanceof SynchronizableRepeater synchronizableRepeater
122                    ? synchronizableRepeater.getEntries()
123                    : List.of(); // repeater can be an UntouchedValue, if it is disable or non-writable
124
125        if (!repeaterEntries.isEmpty())
126        {
127            Set<EducationalPath> paths = new HashSet<>();
128            ModelItem modelItem = content.getDefinition(_repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute);
129            
130            for (int i = 0; i < repeaterEntries.size(); i++)
131            {
132                Map<String, Object> entry = repeaterEntries.get(i);
133                int position = i + 1;
134                
135                Optional<String> oldDataPath = Optional.of(repeaterEntries)
136                                                       .filter(SynchronizableRepeater.class::isInstance)
137                                                       .map(SynchronizableRepeater.class::cast)
138                                                       .flatMap(syncRepeater -> syncRepeater.getPreviousPosition(position))
139                                                       .map(previousPosition -> _repeaterPath + "[" + previousPosition + "]" + ModelItem.ITEM_PATH_SEPARATOR + _educationalPathAttribute);
140                
141                EducationalPath path = Optional.ofNullable(_educationalPathAttribute)
142                                                  .map(entry::get)
143                                                  .map(value -> DataHolderHelper.getValueFromSynchronizableValue(value, content, modelItem, oldDataPath, SynchronizationContext.newInstance()))
144                                                  .filter(EducationalPath.class::isInstance)
145                                                  .map(EducationalPath.class::cast)
146                                                  .orElse(null);   // value can be an UntouchedValue, if it is disabled or non-writable
147                
148                if (path != null && !paths.add(path))
149                {
150                    // Two entries or more have the same value for education path
151                    Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
152                    i18nParams.put("path", new I18nizableText(_odfHelper.getEducationalPathAsString(path)));
153                    result.addError(new I18nizableText(StringUtils.substringBefore(_errorI18nKey, ':'), StringUtils.substringAfter(_errorI18nKey, ':'), i18nParams));
154                    break; // stop iteration
155                }
156            }
157        }
158        
159        return result;
160    }
161    
162}