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}