001/*
002 *  Copyright 2018 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.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import org.apache.avalon.framework.configuration.Configurable;
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034
035import org.ametys.cms.contenttype.validation.AbstractContentValidator;
036import org.ametys.cms.data.ContentValue;
037import org.ametys.cms.repository.Content;
038import org.ametys.odf.ODFHelper;
039import org.ametys.odf.ProgramItem;
040import org.ametys.odf.course.Course;
041import org.ametys.odf.courselist.CourseList;
042import org.ametys.odf.program.ProgramPart;
043import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
044import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
045import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
046import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.i18n.I18nizableTextParameter;
049import org.ametys.runtime.model.ElementDefinition;
050import org.ametys.runtime.model.View;
051import org.ametys.runtime.parameter.ValidationResult;
052
053/**
054 * Global validator for {@link ProgramItem} content.
055 * Check that structure to not create an infinite loop
056 */
057public class ProgramItemHierarchyValidator extends AbstractContentValidator implements Serviceable, Configurable
058{
059    /** The ODF helper */
060    protected ODFHelper _odfHelper;
061    
062    private Set<String> _childAttributeNames;
063    private Set<String> _parentAttributeNames;
064    
065    @Override
066    public void service(ServiceManager smanager) throws ServiceException
067    {
068        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
069    }
070    
071    @Override
072    public ValidationResult validate(Content content)
073    {
074        // Nothing
075        return ValidationResult.empty();
076    }
077    
078    public void configure(Configuration configuration) throws ConfigurationException
079    {
080        _childAttributeNames = _configureAttributeNames(configuration, "childAttribute");
081        _parentAttributeNames = _configureAttributeNames(configuration, "parentAttribute");
082    }
083    
084    private Set<String> _configureAttributeNames(Configuration configuration, String childName) throws ConfigurationException
085    {
086        Set<String> attributeNames = new HashSet<>();
087        
088        Configuration[] children = configuration.getChildren(childName);
089        for (Configuration childConf : children)
090        {
091            attributeNames.add(childConf.getAttribute("name"));
092        }
093        
094        return attributeNames;
095    }
096    
097    @Override
098    public ValidationResult validate(Content content, Map<String, Object> values, View view)
099    {
100        ValidationResult result = new ValidationResult();
101        
102        List<ProgramItem> newParentProgramItems = new ArrayList<>();
103        
104        // Get the new parents from values
105        Set<String> parentAttributeNames = getParentAttributeNames();
106        for (String parentAttributeName : parentAttributeNames)
107        {
108            if (view.hasModelViewItem(parentAttributeName) && content instanceof ProgramItem)
109            {
110                ElementDefinition parentDefinition = (ElementDefinition) content.getDefinition(parentAttributeName);
111                Object value = values.get(parentAttributeName);
112                Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, parentDefinition, Optional.of(parentAttributeName), SynchronizationContext.newInstance());
113                
114                newParentProgramItems = Optional.ofNullable(valueToValidate)
115                        .map(Stream::of)
116                        .orElseGet(Stream::of)
117                        .map(parentDefinition.getType()::castValue)
118                        .map(ContentValue.class::cast)
119                        .map(ContentValue::getContent)
120                        .filter(ProgramItem.class::isInstance)
121                        .map(ProgramItem.class::cast)
122                        .collect(Collectors.toList());
123                
124            }
125        }
126        
127        Set<String> names = getChildAttributeNames();
128        for (String name : names)
129        {
130            if (view.hasModelViewItem(name) && content instanceof ProgramItem)
131            {
132                List<String> childContentIds = _odfHelper.getChildProgramItems((ProgramItem) content).stream()
133                        .map(ProgramItem::getId)
134                        .collect(Collectors.toList());
135                
136                ElementDefinition childDefinition = (ElementDefinition) content.getDefinition(name);
137                
138                Object value = values.get(name);
139                Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, childDefinition, Optional.of(name), SynchronizationContext.newInstance());
140                Mode mode = _getMode(value);
141                
142                // Check duplicate child and infinite loop is not necessary when value is removed
143                if (Mode.REMOVE != mode)
144                {
145                    ContentValue[] contentValues = Optional.ofNullable(valueToValidate)
146                            .map(Stream::of)
147                            .orElseGet(Stream::of)
148                            .map(childDefinition.getType()::castValue)
149                            .toArray(ContentValue[]::new);
150                    for (ContentValue contentValue : contentValues)
151                    {
152                        Content childContent = contentValue.getContent();
153                        
154                        // Check duplicate child only on APPEND mode
155                        if (Mode.APPEND == mode)
156                        {
157                            if (childContentIds.contains(childContent.getId()))
158                            {
159                                I18nizableText errorMessage = _getError(childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_DUPLICATE_CHILD_ERROR");
160                                result.addError(errorMessage);
161                            }
162                        }
163                        
164                        if (childContent instanceof ProgramItem)
165                        {
166                            if (!checkAncestors((ProgramItem) childContent, newParentProgramItems) || !checkAncestors((ProgramItem) content, (ProgramItem) childContent))
167                            {
168                                I18nizableText errorMessage = _getError(childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_HIERARCHY_ERROR");
169                                result.addError(errorMessage);
170                            }
171                        }
172                    }
173                }
174            }
175        }
176        
177        return result;
178    }
179    
180    private Mode _getMode(Object value)
181    {
182        return value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : Mode.REPLACE;
183    }
184    
185    /**
186     * Get the names of child attribute to be checked
187     * @return the child attribute's names
188     */
189    protected Set<String> getChildAttributeNames()
190    {
191        return _childAttributeNames;
192    }
193    
194    /**
195     * Get the names of parent attribute to be checked
196     * @return the parent attribute's names
197     */
198    protected Set<String> getParentAttributeNames()
199    {
200        return _parentAttributeNames;
201    }
202    
203    /**
204     * Check if the hierarchy of a program item will be still valid if adding the given program item as child.
205     * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem}
206     * @param programItem The content to start search
207     * @param childProgramItem The child program item to search in ancestors
208     * @return true if child program item is already part of the hierarchy (ascendant search)
209     */
210    protected boolean checkAncestors(ProgramItem programItem, ProgramItem childProgramItem)
211    {
212        if (programItem.equals(childProgramItem))
213        {
214            return false;
215        }
216
217        List<? extends ProgramItem> parents = new ArrayList<>();
218        if (programItem instanceof Course)
219        {
220            parents = ((Course) programItem).getParentCourseLists();
221        }
222        else if (programItem instanceof CourseList)
223        {
224            parents = ((CourseList) programItem).getParentCourses();
225        }
226        else if (programItem instanceof ProgramPart)
227        {
228            parents = ((ProgramPart) programItem).getProgramPartParents();
229        }
230        
231        return checkAncestors(childProgramItem, parents);
232    }
233    
234    /**
235     * Check if the hierarchy of a program item will be still valid if adding the given program item as child.
236     * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem}
237     * @param childProgramItem The child program item to add
238     * @param parentProgramItems The parent program items of the target
239     * @return true if child program item is already part of the hierarchy (ascendant search)
240     */
241    protected boolean checkAncestors(ProgramItem childProgramItem, List<? extends ProgramItem> parentProgramItems)
242    {
243        for (ProgramItem parent : parentProgramItems)
244        {
245            if (parent.equals(childProgramItem))
246            {
247                return false;
248            }
249            else if (!checkAncestors(parent, childProgramItem))
250            {
251                return false;
252            }
253        }
254        
255        return true;
256    }
257    
258    /**
259     * Retrieves an error message
260     * @param childDefinition The child attribute definition
261     * @param content The content being edited
262     * @param childContent The content to add as child content
263     * @param catalog the i18n catalog 
264     * @param i18nKey the i18n key for the error message
265     * @return the error message
266     */
267    protected I18nizableText _getError(ElementDefinition childDefinition, Content content, Content childContent, String catalog, String i18nKey)
268    {
269        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
270        i18nParams.put("title", new I18nizableText(content.getTitle()));
271        i18nParams.put("childTitle", new I18nizableText(childContent.getTitle()));
272        i18nParams.put("fieldLabel", childDefinition.getLabel());
273        return new I18nizableText(catalog, i18nKey, i18nParams);
274    }
275}