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;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.Set;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import javax.jcr.Node;
029import javax.jcr.NodeIterator;
030import javax.jcr.Repository;
031import javax.jcr.RepositoryException;
032import javax.jcr.Session;
033import javax.jcr.query.InvalidQueryException;
034import javax.jcr.query.Query;
035
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.commons.lang.StringUtils;
041import org.apache.commons.lang3.tuple.Pair;
042
043import org.ametys.cms.data.holder.group.IndexableRepeaterEntry;
044import org.ametys.cms.repository.Content;
045import org.ametys.cms.repository.ModifiableContent;
046import org.ametys.cms.repository.WorkflowAwareContent;
047import org.ametys.cms.workflow.ContentWorkflowHelper;
048import org.ametys.cms.workflow.EditContentFunction;
049import org.ametys.cms.workflow.ValidateContentFunction;
050import org.ametys.odf.data.EducationalPath;
051import org.ametys.odf.data.type.EducationalPathRepositoryElementType;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.RepositoryConstants;
054import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
055import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
056import org.ametys.plugins.repository.data.holder.values.SynchronizationResult;
057import org.ametys.plugins.repository.model.RepeaterDefinition;
058import org.ametys.plugins.repository.provider.AbstractRepository;
059import org.ametys.plugins.repository.query.QueryHelper;
060import org.ametys.plugins.repository.query.expression.AndExpression;
061import org.ametys.plugins.repository.query.expression.Expression;
062import org.ametys.plugins.repository.query.expression.Expression.Operator;
063import org.ametys.plugins.repository.query.expression.StringExpression;
064import org.ametys.plugins.workflow.AbstractWorkflowComponent;
065import org.ametys.plugins.workflow.component.CheckRightsCondition;
066import org.ametys.runtime.model.ModelItem;
067import org.ametys.runtime.plugin.component.AbstractLogEnabled;
068
069import com.opensymphony.workflow.WorkflowException;
070
071/**
072 * Helper for manipulating {@link EducationalPath}
073 */
074public class EducationalPathHelper extends AbstractLogEnabled implements Component, Serviceable
075{
076    /** The avalon role */
077    public static final String ROLE = EducationalPathHelper.class.getName();
078    
079    /** Constant to get/set the ancestor path (may be partial) of a program item in request attribute */
080    public static final String PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR = EducationalPathHelper.class.getName() + "$ancestorPath";
081    
082    /** Constant to get/set the root program item in request attribute */
083    public static final String ROOT_PROGRAM_ITEM_REQUEST_ATTR = EducationalPathHelper.class.getName() + "$rootProgramItem";
084    
085    private AmetysObjectResolver _resolver;
086    private Repository _repository;
087
088    private ContentWorkflowHelper _workflowHelper;
089
090    public void service(ServiceManager manager) throws ServiceException
091    {
092        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
093        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
094        _workflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
095    }
096    
097    /**
098     * Determines if program items is part of a same educational path value
099     * @param programItems the program items to search into a educational path value
100     * @return true if the program items is part of a educational path value
101     * @throws RepositoryException if failed to get educational path nodes
102     */
103    public boolean isPartOfEducationalPath(ProgramItem... programItems) throws RepositoryException
104    {
105        String[] programItemIds = Stream.of(programItems)
106                                        .map(ProgramItem::getId)
107                                        .toArray(String[]::new);
108        return isPartOfEducationalPath(programItemIds);
109    }
110    
111    /**
112     * Determines if program items is part of a same educational path  value
113     * @param programItemIds the id of program items to search into a educational path  value
114     * @return true if the program items is part of a educational path  value
115     * @throws RepositoryException if failed to get educational path nodes
116     */
117    public boolean isPartOfEducationalPath(String... programItemIds) throws RepositoryException
118    {
119        NodeIterator nodeIterator = _getEducationalPathNodes(programItemIds);
120        return nodeIterator.hasNext();
121    }
122    
123    /**
124     * Get the educational paths that reference the given program items
125     * @param programItems the referenced program items to search into a educational path
126     * @return a Map with program items referencing the given program items in a educational path attribute as key and the list of educational path references as value
127     * @throws RepositoryException if an error occurred while retrieving educational path references
128     */
129    public Map<ProgramItem, List<EducationalPathReference>> getEducationalPathReferences(ProgramItem... programItems) throws RepositoryException
130    {
131        String[] programItemIds = Stream.of(programItems)
132                .map(ProgramItem::getId)
133                .toArray(String[]::new);
134        return getEducationalPathReferences(programItemIds);
135    }
136    
137    /**
138     * Get the educational paths that reference the given program items
139     * @param programItemIds the id of program items to search into a educational path
140     * @return a Map with program items referencing the given program items in a educational path attribute as key and the list of educational path references as value
141     * @throws RepositoryException if an error occurred while retrieving educational path references
142     */
143    public Map<ProgramItem, List<EducationalPathReference>> getEducationalPathReferences(String... programItemIds) throws RepositoryException
144    {
145        Map<ProgramItem, List<EducationalPathReference>> references = new HashMap<>();
146        
147        NodeIterator educationalPathNodesIterator = _getEducationalPathNodes(programItemIds);
148        
149        while (educationalPathNodesIterator.hasNext())
150        {
151            Node educationalPathNode = educationalPathNodesIterator.nextNode();
152            String educationalPathAttributeName = StringUtils.substringAfter(educationalPathNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
153            
154            Node repeaterEntryNode = educationalPathNode.getParent();
155            
156            Pair<ProgramItem, String> resolvedEntryPath = _resolveEntryPath(repeaterEntryNode);
157            
158            ProgramItem refProgramItem = resolvedEntryPath.getLeft();
159            if (!references.containsKey(refProgramItem))
160            {
161                references.put(refProgramItem, new ArrayList<>());
162            }
163            
164            EducationalPath value = ((Content) refProgramItem).getValue(resolvedEntryPath.getRight() + "/" + educationalPathAttributeName);
165            references.get(refProgramItem).add(new EducationalPathReference(refProgramItem, resolvedEntryPath.getRight(), value));
166        }
167        
168        return references;
169    }
170    
171    /**
172     * Remove all repeater entries with a educational path that reference the given program items
173     * @param programItems the referenced program items to search into a educational path
174     * @throws RepositoryException if an error occurred while retrieving educational path references
175     * @throws WorkflowException if failed to do workflow action on modified contents
176     */
177    public void removeEducationalPathReferences(ProgramItem... programItems) throws RepositoryException, WorkflowException
178    {
179        String[] programItemIds = Stream.of(programItems)
180                .map(ProgramItem::getId)
181                .toArray(String[]::new);
182        removeEducationalPathReferences(2, programItemIds);
183    }
184    
185    /**
186     * Remove all repeater entries with a educational path that reference the given program items
187     * @param programItemIds the id program items to search into a educational path
188     * @throws RepositoryException if an error occurred while retrieving educational path references
189     * @throws WorkflowException if failed to do workflow action on modified contents
190     */
191    public void removeEducationalPathReferences(List<String> programItemIds) throws RepositoryException, WorkflowException
192    {
193        removeEducationalPathReferences(2, programItemIds.toArray(String[]::new));
194    }
195    
196    /**
197     * Remove all repeater entries with a educational path that reference the given program items
198     * @param workflowActionId The id of the workflow action to do
199     * @param programItemIds the ids of program items to search into a educational path
200     * @throws RepositoryException if an error occurred while retrieving educational path references
201     * @throws WorkflowException if failed to do workflow action on modified contents
202     */
203    public void removeEducationalPathReferences(int workflowActionId, String... programItemIds) throws RepositoryException, WorkflowException
204    {
205        Map<ProgramItem, List<EducationalPathReference>> educationalPathReferences = getEducationalPathReferences(programItemIds);
206        
207        for (Entry<ProgramItem, List<EducationalPathReference>> entry : educationalPathReferences.entrySet())
208        {
209            ProgramItem refProframItem = entry.getKey();
210            
211            List<EducationalPathReference> references = entry.getValue();
212            for (EducationalPathReference educationalPathReference : references)
213            {
214                String repeaterEntryPath = educationalPathReference.repeaterEntryPath();
215                ((ModifiableContent) refProframItem).removeValue(repeaterEntryPath);
216            }
217            
218            _triggerWorkflowAction((Content) refProframItem, 2);
219        }
220    }
221    
222    
223    /**
224     * Remove all repeaters with a educational path in their model
225     * @param programItem the program item
226     * @throws WorkflowException if failed to do workflow action on modified contents
227     */
228    public void removeAllRepeatersWithEducationalPath(ProgramItem programItem) throws WorkflowException
229    {
230        ModifiableContent content = (ModifiableContent) programItem;
231        
232        boolean needSave = false;
233        
234        Map<String, Object> educationPaths = DataHolderHelper.findItemsByType(content, EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID);
235        
236        // Get path of repeaters with a education path value
237        Set<String> repeaterDataPaths = educationPaths.entrySet().stream()
238            .filter(e -> e.getValue() instanceof EducationalPath)
239            .map(Entry::getKey) // data path
240            .map(dataPath -> StringUtils.substring(dataPath, 0, dataPath.lastIndexOf("/"))) // parent data path
241            .filter(DataHolderHelper::isRepeaterEntryPath)
242            .map(DataHolderHelper::getRepeaterNameAndEntryPosition) // get repeater path
243            .map(Pair::getLeft)
244            .collect(Collectors.toSet());
245        
246        for (String repeaterDataPath : repeaterDataPaths)
247        {
248            if (content.hasValue(repeaterDataPath))
249            {
250                content.removeValue(repeaterDataPath);
251                needSave = true;
252            }
253        }
254        
255        if (needSave)
256        {
257            _triggerWorkflowAction(content, 2);
258        }
259    }
260    
261    
262    /**
263     * Determines if this repeater is a repeater with a educational path
264     * @param repeaterDefinition the repeater definition
265     * @return true if the repeater contains data related to specific an education path
266     */
267    public boolean isRepeaterWithEducationalPath(RepeaterDefinition repeaterDefinition)
268    {
269        for (ModelItem childItem : repeaterDefinition.getModelItems())
270        {
271            if (EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID.equals(childItem.getType().getId()))
272            {
273                return true;
274            }
275        }
276        return false;
277    }
278    
279    /**
280     * Browse entries of a repeater with educational path to get their values by educational path
281     * @param content The content
282     * @param repeaterDefinition The repeater definition
283     * @return a Map of {@link EducationalPath} with related values by names
284     */
285    public Map<EducationalPath, Map<String, Object>> getValuesByEducationalPath(Content content, RepeaterDefinition repeaterDefinition)
286    {
287        Map<EducationalPath, Map<String, Object>> valuesByEducationalPath = new HashMap<>();
288        
289        String repeaterPath = repeaterDefinition.getPath();
290        if (content.hasValue(repeaterPath) && isRepeaterWithEducationalPath(repeaterDefinition))
291        {
292            List< ? extends IndexableRepeaterEntry> entries = content.getRepeater(repeaterPath).getEntries();
293            for (IndexableRepeaterEntry entry : entries)
294            {
295                EducationalPath path = null;
296                Map<String, Object> values = new HashMap<>();
297                for (ModelItem childItem : repeaterDefinition.getModelItems())
298                {
299                    if (EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID.equals(childItem.getType().getId()))
300                    {
301                        path = entry.getValue(childItem.getName());
302                    }
303                    else if (entry.hasValue(childItem.getName()))
304                    {
305                        values.put(childItem.getName(), entry.getValue(childItem.getName()));
306                    }
307                }
308                
309                if (path != null)
310                {
311                    valuesByEducationalPath.put(path, values);
312                }
313            }
314        }
315        return valuesByEducationalPath;
316    }
317    
318    /**
319     * Browse a repeater entries' values to reorganize values by educational path
320     * @param repeaterValues The repeater entries' values
321     * @param repeaterDefinition The repeater definition
322     * @return a Map of {@link EducationalPath} with related values by names
323     */
324    public Map<EducationalPath, Map<String, Object>> getValuesByEducationalPath(List<Map<String, Object>> repeaterValues, RepeaterDefinition repeaterDefinition)
325    {
326        Map<EducationalPath, Map<String, Object>> valuesByEducationalPath = new HashMap<>();
327        
328        for (Map<String, Object> entryValues : repeaterValues)
329        {
330            EducationalPath path = null;
331            Map<String, Object> values = new HashMap<>();
332            
333            for (ModelItem childItem : repeaterDefinition.getModelItems())
334            {
335                if (entryValues.containsKey(childItem.getName()))
336                {
337                    Object value = entryValues.get(childItem.getName());
338                    value = value instanceof SynchronizableValue synchronizableValue ? synchronizableValue.getLocalValue() : value;
339                    
340                    if (value instanceof EducationalPath educationPath)
341                    {
342                        path = educationPath;
343                    }
344                    else
345                    {
346                        values.put(childItem.getName(), value);
347                    }
348                }
349            }
350            
351            if (path != null)
352            {
353                valuesByEducationalPath.put(path, values);
354            }
355        }
356        
357        return valuesByEducationalPath;
358    }
359    
360    private void _triggerWorkflowAction(Content content, int actionId) throws WorkflowException
361    {
362        if (content instanceof WorkflowAwareContent)
363        {
364            // The content has already been modified by this function
365            SynchronizationResult synchronizationResult = new SynchronizationResult();
366            synchronizationResult.setHasChanged(true);
367
368            Map<String, Object> parameters = new HashMap<>();
369            parameters.put(ValidateContentFunction.IS_MAJOR, false);
370            parameters.put(EditContentFunction.SYNCHRONIZATION_RESULT, synchronizationResult);
371            parameters.put(EditContentFunction.QUIT, true);
372
373            Map<String, Object> inputs = new HashMap<>();
374            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
375            
376            // Do not check right
377            inputs.put(CheckRightsCondition.FORCE, true);
378            
379            _workflowHelper.doAction((WorkflowAwareContent) content, actionId, inputs);
380        }
381    }
382    
383    private Pair<ProgramItem, String> _resolveEntryPath(Node entryNode) throws RepositoryException
384    {
385        List<String> entryPath = new ArrayList<>();
386        
387        String entryPos = StringUtils.substringAfter(entryNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
388        Node repeaterNode = entryNode.getParent();
389        String repeaterName = StringUtils.substringAfter(repeaterNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
390        
391        entryPath.add(repeaterName + "[" + entryPos + "]");
392        
393        Node currentNode = repeaterNode.getParent();
394        
395        while (!currentNode.isNodeType("ametys:content"))
396        {
397            String currentNodeName = StringUtils.substringAfter(currentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
398            Node parentNode = currentNode.getParent();
399            
400            if (parentNode.isNodeType("ametys:repeater"))
401            {
402                entryPath.add(StringUtils.substringAfter(parentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":") + "[" + currentNodeName + "]");
403                currentNode = parentNode.getParent();
404            }
405            else
406            {
407                entryPath.add(currentNodeName);
408                currentNode = parentNode;
409            }
410        }
411        
412        ProgramItem programItem = _resolver.resolve(currentNode, false);
413        
414        Collections.reverse(entryPath);
415        return Pair.of(programItem, StringUtils.join(entryPath, "/"));
416    }
417    
418    private NodeIterator _getEducationalPathNodes(String... programItemIds) throws InvalidQueryException, RepositoryException
419    {
420        List<Expression> exprs = new ArrayList<>();
421        for (String programItemId : programItemIds)
422        {
423            exprs.add(new StringExpression(EducationalPath.PATH_SEGMENTS_IDENTIFIER, Operator.EQ, programItemId));
424        }
425        
426        String xPathQuery = QueryHelper.getXPathQuery(null, EducationalPathRepositoryElementType.EDUCATIONAL_PATH_NODETYPE, new AndExpression(exprs.toArray(Expression[]::new)));
427        
428        Session session = _repository.login();
429        Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
430        
431        return query.execute().getNodes();
432    }
433    
434    /**
435     * Record for a {@link EducationalPath} references
436     * @param programItem The program item holding the educational path
437     * @param repeaterEntryPath The path of repeater entry holding the the educational path
438     * @param value The value of the educational path
439     */
440    public record EducationalPathReference(ProgramItem programItem, String repeaterEntryPath, EducationalPath value) { /* empty */ }
441}