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.Set;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import org.apache.avalon.framework.configuration.Configurable;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033
034import org.ametys.cms.contenttype.validation.AbstractContentValidator;
035import org.ametys.cms.data.ContentValue;
036import org.ametys.cms.repository.Content;
037import org.ametys.odf.ODFHelper;
038import org.ametys.odf.ProgramItem;
039import org.ametys.odf.course.Course;
040import org.ametys.odf.courselist.CourseList;
041import org.ametys.odf.program.ProgramPart;
042import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
043import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
044import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.i18n.I18nizableTextParameter;
047import org.ametys.runtime.model.ElementDefinition;
048import org.ametys.runtime.model.View;
049import org.ametys.runtime.parameter.Errors;
050
051/**
052 * Global validator for {@link ProgramItem} content.
053 * Check that structure to not create an infinite loop
054 */
055public class ProgramItemHierarchyValidator extends AbstractContentValidator implements Serviceable, Configurable
056{
057    /** The ODF helper */
058    protected ODFHelper _odfHelper;
059    
060    private Set<String> _childMetadataNames;
061    
062    @Override
063    public void service(ServiceManager smanager) throws ServiceException
064    {
065        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
066    }
067    
068    @Override
069    public void validate(Content content, Errors errors)
070    {
071        // Nothing
072    }
073    
074    public void configure(Configuration configuration) throws ConfigurationException
075    {
076        _childMetadataNames = new HashSet<>();
077        
078        Configuration[] children = configuration.getChildren("childMetadata");
079        for (Configuration childConf : children)
080        {
081            _childMetadataNames.add(childConf.getAttribute("name"));
082        }
083    }
084    
085    @Override
086    public void validate(Content content, Map<String, Object> values, View view, Errors errors)
087    {
088        Set<String> names = getChildMetadataNames();
089        for (String name : names)
090        {
091            if (view.hasModelViewItem(name) && content instanceof ProgramItem)
092            {
093                List<String> childContentIds = _odfHelper.getChildProgramItems((ProgramItem) content).stream()
094                        .map(ProgramItem::getId)
095                        .collect(Collectors.toList());
096                
097                ElementDefinition childDefinition = (ElementDefinition) content.getDefinition(name);
098                
099                Object value = values.get(name);
100                Object[] valueToValidate = (Object[]) DataHolderHelper.getValueToValidate(value);
101                Mode mode = _getMode(value);
102                
103                // Check duplicate child and infinite loop is not necessary when value is removed
104                if (Mode.REMOVE != mode)
105                {
106                    ContentValue[] contentValues = Stream.of(valueToValidate)
107                            .map(v -> childDefinition.getType().castValue(v))
108                            .toArray(ContentValue[]::new);
109                    for (ContentValue contentValue : contentValues)
110                    {
111                        Content childContent = contentValue.getContent();
112                        
113                        // Check duplicate child only on APPEND mode
114                        if (Mode.APPEND == mode)
115                        {
116                            if (childContentIds.contains(childContent.getId()))
117                            {
118                                _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_DUPLICATE_CHILD_ERROR");
119                            }
120                        }
121                        
122                        if (childContent instanceof ProgramItem)
123                        {
124                            if (!checkAncestors((ProgramItem) content, (ProgramItem) childContent))
125                            {
126                                _addError(errors, childDefinition, content, childContent, "plugin.odf", "PLUGINS_ODF_CONTENT_VALIDATOR_HIERARCHY_ERROR");
127                            }
128                        }
129                    }
130                }
131            }
132        }
133    }
134    
135    private Mode _getMode(Object value)
136    {
137        return value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : Mode.REPLACE;
138    }
139    
140    /**
141     * Get the names of child metadata to be checked
142     * @return the child metadata's names
143     */
144    protected Set<String> getChildMetadataNames()
145    {
146        return _childMetadataNames;
147    }
148    
149    /**
150     * Check if the hierarchy of a program item will be still valid if adding the given program item as child.
151     * Return false if the {@link ProgramItem} to add is in the hierarchy of the given {@link ProgramItem}
152     * @param programItem The content to start search
153     * @param childProgramItem The child program item to search in ancestors
154     * @return true if child program item is already part of the hierarchy (ascendant search)
155     */
156    protected boolean checkAncestors(ProgramItem programItem, ProgramItem childProgramItem)
157    {
158        if (programItem.equals(childProgramItem))
159        {
160            return false;
161        }
162
163        List<? extends ProgramItem> parents = new ArrayList<>();
164        if (programItem instanceof Course)
165        {
166            parents = ((Course) programItem).getParentCourseLists();
167        }
168        else if (programItem instanceof CourseList)
169        {
170            parents = ((CourseList) programItem).getParentCourses();
171        }
172        else if (programItem instanceof ProgramPart)
173        {
174            parents = ((ProgramPart) programItem).getProgramPartParents();
175        }
176        
177        for (ProgramItem parent : parents)
178        {
179            if (parent.equals(childProgramItem))
180            {
181                return false;
182            }
183            else if (!checkAncestors(parent, childProgramItem))
184            {
185                return false;
186            }
187        }
188        
189        return true;
190    }
191    
192    /**
193     * Add an error
194     * @param errors The list of errors
195     * @param childDefinition The child metadata definition
196     * @param content The content being edited
197     * @param childContent The content to add as child content
198     * @param catalog the i18n catalog 
199     * @param i18nKey the i18n key for the error message
200     */
201    protected void _addError(Errors errors, ElementDefinition childDefinition, Content content, Content childContent, String catalog, String i18nKey)
202    {
203        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
204        i18nParams.put("title", new I18nizableText(content.getTitle()));
205        i18nParams.put("childTitle", new I18nizableText(childContent.getTitle()));
206        i18nParams.put("fieldLabel", childDefinition.getLabel());
207        errors.addError(new I18nizableText(catalog, i18nKey, i18nParams));
208    }
209}