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