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}