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.Errors;
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> _childMetadataNames;
063    
064    private Set<String> _parentMetadataNames;
065    
066    @Override
067    public void service(ServiceManager smanager) throws ServiceException
068    {
069        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
070    }
071    
072    @Override
073    public void validate(Content content, Errors errors)
074    {
075        // Nothing
076    }
077    
078    public void configure(Configuration configuration) throws ConfigurationException
079    {
080        _childMetadataNames = new HashSet<>();
081        _parentMetadataNames = new HashSet<>();
082        
083        Configuration[] children = configuration.getChildren("childMetadata");
084        for (Configuration childConf : children)
085        {
086            _childMetadataNames.add(childConf.getAttribute("name"));
087        }
088        
089        children = configuration.getChildren("parentMetadata");
090        for (Configuration childConf : children)
091        {
092            _parentMetadataNames.add(childConf.getAttribute("name"));
093        }
094    }
095    
096    @Override
097    public void validate(Content content, Map<String, Object> values, View view, Errors errors)
098    {
099        List<ProgramItem> newParentProgramItems = new ArrayList<>();
100        
101        // Get the new parents from values
102        Set<String> parentMetadataNames = getParentMetadataNames();
103        for (String parentMetadataName : parentMetadataNames)
104        {
105            if (view.hasModelViewItem(parentMetadataName) && content instanceof ProgramItem)
106            {
107                ElementDefinition parentDefinition = (ElementDefinition) content.getDefinition(parentMetadataName);
108                Object value = values.get(parentMetadataName);
109                Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, parentDefinition, Optional.of(parentMetadataName), SynchronizationContext.newInstance());
110                
111                newParentProgramItems = Optional.ofNullable(valueToValidate)
112                        .map(Stream::of)
113                        .orElseGet(Stream::of)
114                        .map(parentDefinition.getType()::castValue)
115                        .map(ContentValue.class::cast)
116                        .map(ContentValue::getContent)
117                        .filter(ProgramItem.class::isInstance)
118                        .map(ProgramItem.class::cast)
119                        .collect(Collectors.toList());
120                
121            }
122        }
123        
124        Set<String> names = getChildMetadataNames();
125        for (String name : names)
126        {
127            if (view.hasModelViewItem(name) && content instanceof ProgramItem)
128            {
129                List<String> childContentIds = _odfHelper.getChildProgramItems((ProgramItem) content).stream()
130                        .map(ProgramItem::getId)
131                        .collect(Collectors.toList());
132                
133                ElementDefinition childDefinition = (ElementDefinition) content.getDefinition(name);
134                
135                Object value = values.get(name);
136                Object[] valueToValidate = (Object[]) DataHolderHelper.getValueFromSynchronizableValue(value, content, childDefinition, Optional.of(name), SynchronizationContext.newInstance());
137                Mode mode = _getMode(value);
138                
139                // Check duplicate child and infinite loop is not necessary when value is removed
140                if (Mode.REMOVE != mode)
141                {
142                    ContentValue[] contentValues = Optional.ofNullable(valueToValidate)
143                            .map(Stream::of)
144                            .orElseGet(Stream::of)
145                            .map(childDefinition.getType()::castValue)
146                            .toArray(ContentValue[]::new);
147                    for (ContentValue contentValue : contentValues)
148                    {
149                        Content childContent = contentValue.getContent();
150                        
151                        // Check duplicate child only on APPEND mode
152                        if (Mode.APPEND == mode)
153                        {
154                            if (childContentIds.contains(childContent.getId()))
155                            {
156                                _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_DUPLICATE_CHILD_ERROR");
157                            }
158                        }
159                        
160                        if (childContent instanceof ProgramItem)
161                        {
162                            if (!checkAncestors((ProgramItem) childContent, newParentProgramItems) || !checkAncestors((ProgramItem) content, (ProgramItem) childContent))
163                            {
164                                _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_HIERARCHY_ERROR");
165                            }
166                        }
167                    }
168                }
169            }
170        }
171    }
172    
173    private Mode _getMode(Object value)
174    {
175        return value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : Mode.REPLACE;
176    }
177    
178    /**
179     * Get the names of child metadata to be checked
180     * @return the child metadata's names
181     */
182    protected Set<String> getChildMetadataNames()
183    {
184        return _childMetadataNames;
185    }
186    
187    /**
188     * Get the names of parent metadata to be checked
189     * @return the parent metadata's names
190     */
191    protected Set<String> getParentMetadataNames()
192    {
193        return _parentMetadataNames;
194    }
195    
196    /**
197     * Check if the hierarchy of a program item will be still valid if adding the given program item as child.
198     * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem}
199     * @param programItem The content to start search
200     * @param childProgramItem The child program item to search in ancestors
201     * @return true if child program item is already part of the hierarchy (ascendant search)
202     */
203    protected boolean checkAncestors(ProgramItem programItem, ProgramItem childProgramItem)
204    {
205        if (programItem.equals(childProgramItem))
206        {
207            return false;
208        }
209
210        List<? extends ProgramItem> parents = new ArrayList<>();
211        if (programItem instanceof Course)
212        {
213            parents = ((Course) programItem).getParentCourseLists();
214        }
215        else if (programItem instanceof CourseList)
216        {
217            parents = ((CourseList) programItem).getParentCourses();
218        }
219        else if (programItem instanceof ProgramPart)
220        {
221            parents = ((ProgramPart) programItem).getProgramPartParents();
222        }
223        
224        return checkAncestors(childProgramItem, parents);
225    }
226    
227    /**
228     * Check if the hierarchy of a program item will be still valid if adding the given program item as child.
229     * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem}
230     * @param childProgramItem The child program item to add
231     * @param parentProgramItems The parent program items of the target
232     * @return true if child program item is already part of the hierarchy (ascendant search)
233     */
234    protected boolean checkAncestors(ProgramItem childProgramItem, List<? extends ProgramItem> parentProgramItems)
235    {
236        for (ProgramItem parent : parentProgramItems)
237        {
238            if (parent.equals(childProgramItem))
239            {
240                return false;
241            }
242            else if (!checkAncestors(parent, childProgramItem))
243            {
244                return false;
245            }
246        }
247        
248        return true;
249    }
250    
251    /**
252     * Add an error
253     * @param errors The list of errors
254     * @param childDefinition The child metadata definition
255     * @param content The content being edited
256     * @param childContent The content to add as child content
257     * @param catalog the i18n catalog 
258     * @param i18nKey the i18n key for the error message
259     */
260    protected void _addError(Errors errors, ElementDefinition childDefinition, Content content, Content childContent, String catalog, String i18nKey)
261    {
262        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
263        i18nParams.put("title", new I18nizableText(content.getTitle()));
264        i18nParams.put("childTitle", new I18nizableText(childContent.getTitle()));
265        i18nParams.put("fieldLabel", childDefinition.getLabel());
266        errors.addError(new I18nizableText(catalog, i18nKey, i18nParams));
267    }
268}