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.repository.Content;
044import org.ametys.cms.repository.ModifiableContent;
045import org.ametys.cms.repository.WorkflowAwareContent;
046import org.ametys.cms.workflow.ContentWorkflowHelper;
047import org.ametys.cms.workflow.EditContentFunction;
048import org.ametys.cms.workflow.ValidateContentFunction;
049import org.ametys.odf.data.EducationalPath;
050import org.ametys.odf.data.type.EducationalPathRepositoryElementType;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.repository.RepositoryConstants;
053import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
054import org.ametys.plugins.repository.data.holder.values.SynchronizationResult;
055import org.ametys.plugins.repository.provider.AbstractRepository;
056import org.ametys.plugins.repository.query.QueryHelper;
057import org.ametys.plugins.repository.query.expression.AndExpression;
058import org.ametys.plugins.repository.query.expression.Expression;
059import org.ametys.plugins.repository.query.expression.Expression.Operator;
060import org.ametys.plugins.repository.query.expression.StringExpression;
061import org.ametys.plugins.workflow.AbstractWorkflowComponent;
062import org.ametys.plugins.workflow.component.CheckRightsCondition;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064
065import com.opensymphony.workflow.WorkflowException;
066
067/**
068 * Helper for manipulating {@link EducationalPath}
069 */
070public class EducationalPathHelper extends AbstractLogEnabled implements Component, Serviceable
071{
072    /** The avalon role */
073    public static final String ROLE = EducationalPathHelper.class.getName();
074    
075    /** Constant to get/set the ancestor path (may be partial) of a program item in request attribute */
076    public static final String PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR = EducationalPathHelper.class.getName() + "$ancestorPath";
077    
078    /** Constant to get/set the root program item in request attribute */
079    public static final String ROOT_PROGRAM_ITEM_REQUEST_ATTR = EducationalPathHelper.class.getName() + "$rootProgramItem";
080    
081    private AmetysObjectResolver _resolver;
082    private Repository _repository;
083
084    private ContentWorkflowHelper _workflowHelper;
085
086    public void service(ServiceManager manager) throws ServiceException
087    {
088        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
089        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
090        _workflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
091    }
092    
093    /**
094     * Determines if program items is part of a same educational path value
095     * @param programItems the program items to search into a educational path value
096     * @return true if the program items is part of a educational path value
097     * @throws RepositoryException if failed to get educational path nodes
098     */
099    public boolean isPartOfEducationalPath(ProgramItem... programItems) throws RepositoryException
100    {
101        String[] programItemIds = Stream.of(programItems)
102                                        .map(ProgramItem::getId)
103                                        .toArray(String[]::new);
104        return isPartOfEducationalPath(programItemIds);
105    }
106    
107    /**
108     * Determines if program items is part of a same educational path  value
109     * @param programItemIds the id of program items to search into a educational path  value
110     * @return true if the program items is part of a educational path  value
111     * @throws RepositoryException if failed to get educational path nodes
112     */
113    public boolean isPartOfEducationalPath(String... programItemIds) throws RepositoryException
114    {
115        NodeIterator nodeIterator = _getEducationalPathNodes(programItemIds);
116        return nodeIterator.hasNext();
117    }
118    
119    /**
120     * Get the educational paths that reference the given program items
121     * @param programItems the referenced program items to search into a educational path
122     * @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
123     * @throws RepositoryException if an error occurred while retrieving educational path references
124     */
125    public Map<ProgramItem, List<EducationalPathReference>> getEducationalPathReferences(ProgramItem... programItems) throws RepositoryException
126    {
127        String[] programItemIds = Stream.of(programItems)
128                .map(ProgramItem::getId)
129                .toArray(String[]::new);
130        return getEducationalPathReferences(programItemIds);
131    }
132    
133    /**
134     * Get the educational paths that reference the given program items
135     * @param programItemIds the id of program items to search into a educational path
136     * @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
137     * @throws RepositoryException if an error occurred while retrieving educational path references
138     */
139    public Map<ProgramItem, List<EducationalPathReference>> getEducationalPathReferences(String... programItemIds) throws RepositoryException
140    {
141        Map<ProgramItem, List<EducationalPathReference>> references = new HashMap<>();
142        
143        NodeIterator educationalPathNodesIterator = _getEducationalPathNodes(programItemIds);
144        
145        while (educationalPathNodesIterator.hasNext())
146        {
147            Node educationalPathNode = educationalPathNodesIterator.nextNode();
148            String educationalPathAttributeName = StringUtils.substringAfter(educationalPathNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
149            
150            Node repeaterEntryNode = educationalPathNode.getParent();
151            
152            Pair<ProgramItem, String> resolvedEntryPath = _resolveEntryPath(repeaterEntryNode);
153            
154            ProgramItem refProgramItem = resolvedEntryPath.getLeft();
155            if (!references.containsKey(refProgramItem))
156            {
157                references.put(refProgramItem, new ArrayList<>());
158            }
159            
160            EducationalPath value = ((Content) refProgramItem).getValue(resolvedEntryPath.getRight() + "/" + educationalPathAttributeName);
161            references.get(refProgramItem).add(new EducationalPathReference(refProgramItem, resolvedEntryPath.getRight(), value));
162        }
163        
164        return references;
165    }
166    
167    /**
168     * Remove all repeater entries with a educational path that reference the given program items
169     * @param programItems the referenced program items to search into a educational path
170     * @throws RepositoryException if an error occurred while retrieving educational path references
171     * @throws WorkflowException if failed to do workflow action on modified contents
172     */
173    public void removeEducationalPathReferences(ProgramItem... programItems) throws RepositoryException, WorkflowException
174    {
175        String[] programItemIds = Stream.of(programItems)
176                .map(ProgramItem::getId)
177                .toArray(String[]::new);
178        removeEducationalPathReferences(2, programItemIds);
179    }
180    
181    /**
182     * Remove all repeater entries with a educational path that reference the given program items
183     * @param programItemIds the id program items to search into a educational path
184     * @throws RepositoryException if an error occurred while retrieving educational path references
185     * @throws WorkflowException if failed to do workflow action on modified contents
186     */
187    public void removeEducationalPathReferences(List<String> programItemIds) throws RepositoryException, WorkflowException
188    {
189        removeEducationalPathReferences(2, programItemIds.toArray(String[]::new));
190    }
191    
192    /**
193     * Remove all repeater entries with a educational path that reference the given program items
194     * @param workflowActionId The id of the workflow action to do
195     * @param programItemIds the ids of program items to search into a educational path
196     * @throws RepositoryException if an error occurred while retrieving educational path references
197     * @throws WorkflowException if failed to do workflow action on modified contents
198     */
199    public void removeEducationalPathReferences(int workflowActionId, String... programItemIds) throws RepositoryException, WorkflowException
200    {
201        Map<ProgramItem, List<EducationalPathReference>> educationalPathReferences = getEducationalPathReferences(programItemIds);
202        
203        for (Entry<ProgramItem, List<EducationalPathReference>> entry : educationalPathReferences.entrySet())
204        {
205            ProgramItem refProframItem = entry.getKey();
206            
207            List<EducationalPathReference> references = entry.getValue();
208            for (EducationalPathReference educationalPathReference : references)
209            {
210                String repeaterEntryPath = educationalPathReference.repeaterEntryPath();
211                ((ModifiableContent) refProframItem).removeValue(repeaterEntryPath);
212            }
213            
214            _triggerWorkflowAction((Content) refProframItem, 2);
215        }
216    }
217    
218    
219    /**
220     * Remove all repeaters with a educational path in their model
221     * @param programItem the program item
222     * @throws WorkflowException if failed to do workflow action on modified contents
223     */
224    public void removeAllRepeatersWithEducationalPath(ProgramItem programItem) throws WorkflowException
225    {
226        ModifiableContent content = (ModifiableContent) programItem;
227        
228        boolean needSave = false;
229        
230        Map<String, Object> educationPaths = DataHolderHelper.findItemsByType(content, EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID);
231        
232        // Get path of repeaters with a education path value
233        Set<String> repeaterDataPaths = educationPaths.entrySet().stream()
234            .filter(e -> e.getValue() instanceof EducationalPath)
235            .map(Entry::getKey) // data path
236            .map(dataPath -> StringUtils.substring(dataPath, 0, dataPath.lastIndexOf("/"))) // parent data path
237            .filter(DataHolderHelper::isRepeaterEntryPath)
238            .map(DataHolderHelper::getRepeaterNameAndEntryPosition) // get repeater path
239            .map(Pair::getLeft)
240            .collect(Collectors.toSet());
241        
242        for (String repeaterDataPath : repeaterDataPaths)
243        {
244            if (content.hasValue(repeaterDataPath))
245            {
246                content.removeValue(repeaterDataPath);
247                needSave = true;
248            }
249        }
250        
251        if (needSave)
252        {
253            _triggerWorkflowAction(content, 2);
254        }
255    }
256    
257    private void _triggerWorkflowAction(Content content, int actionId) throws WorkflowException
258    {
259        if (content instanceof WorkflowAwareContent)
260        {
261            // The content has already been modified by this function
262            SynchronizationResult synchronizationResult = new SynchronizationResult();
263            synchronizationResult.setHasChanged(true);
264
265            Map<String, Object> parameters = new HashMap<>();
266            parameters.put(ValidateContentFunction.IS_MAJOR, false);
267            parameters.put(EditContentFunction.SYNCHRONIZATION_RESULT, synchronizationResult);
268            parameters.put(EditContentFunction.QUIT, true);
269
270            Map<String, Object> inputs = new HashMap<>();
271            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
272            
273            // Do not check right
274            inputs.put(CheckRightsCondition.FORCE, true);
275            
276            _workflowHelper.doAction((WorkflowAwareContent) content, actionId, inputs);
277        }
278    }
279    
280    private Pair<ProgramItem, String> _resolveEntryPath(Node entryNode) throws RepositoryException
281    {
282        List<String> entryPath = new ArrayList<>();
283        
284        String entryPos = StringUtils.substringAfter(entryNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
285        Node repeaterNode = entryNode.getParent();
286        String repeaterName = StringUtils.substringAfter(repeaterNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
287        
288        entryPath.add(repeaterName + "[" + entryPos + "]");
289        
290        Node currentNode = repeaterNode.getParent();
291        
292        while (!currentNode.isNodeType("ametys:content"))
293        {
294            String currentNodeName = StringUtils.substringAfter(currentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":");
295            Node parentNode = currentNode.getParent();
296            
297            if (parentNode.isNodeType("ametys:repeater"))
298            {
299                entryPath.add(StringUtils.substringAfter(parentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":") + "[" + currentNodeName + "]");
300                currentNode = parentNode.getParent();
301            }
302            else
303            {
304                entryPath.add(currentNodeName);
305                currentNode = parentNode;
306            }
307        }
308        
309        ProgramItem programItem = _resolver.resolve(currentNode, false);
310        
311        Collections.reverse(entryPath);
312        return Pair.of(programItem, StringUtils.join(entryPath, "/"));
313    }
314    
315    private NodeIterator _getEducationalPathNodes(String... programItemIds) throws InvalidQueryException, RepositoryException
316    {
317        List<Expression> exprs = new ArrayList<>();
318        for (String programItemId : programItemIds)
319        {
320            exprs.add(new StringExpression(EducationalPath.PATH_SEGMENTS_IDENTIFIER, Operator.EQ, programItemId));
321        }
322        
323        String xPathQuery = QueryHelper.getXPathQuery(null, EducationalPathRepositoryElementType.EDUCATIONAL_PATH_NODETYPE, new AndExpression(exprs.toArray(Expression[]::new)));
324        
325        Session session = _repository.login();
326        Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
327        
328        return query.execute().getNodes();
329    }
330    
331    /**
332     * Record for a {@link EducationalPath} references
333     * @param programItem The program item holding the educational path
334     * @param repeaterEntryPath The path of repeater entry holding the the educational path
335     * @param value The value of the educational path
336     */
337    public record EducationalPathReference(ProgramItem programItem, String repeaterEntryPath, EducationalPath value) { /* empty */ }
338}