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