001/* 002 * Copyright 2025 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.catalog; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.stream.Stream; 025 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029 030import org.ametys.cms.ObservationConstants; 031import org.ametys.cms.data.ContentValue; 032import org.ametys.cms.repository.Content; 033import org.ametys.cms.repository.ContentQueryHelper; 034import org.ametys.cms.repository.ContentTypeExpression; 035import org.ametys.cms.repository.DefaultContent; 036import org.ametys.cms.repository.ModifiableContent; 037import org.ametys.cms.repository.ModifiableDefaultContent; 038import org.ametys.cms.repository.WorkflowAwareContent; 039import org.ametys.cms.workflow.ContentWorkflowHelper; 040import org.ametys.cms.workflow.EditContentFunction; 041import org.ametys.core.observation.Event; 042import org.ametys.core.observation.ObservationManager; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.odf.program.Program; 045import org.ametys.odf.skill.ODFSkillsHelper; 046import org.ametys.odf.skill.workflow.SkillEditionFunction; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.ModifiableAmetysObject; 050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 051import org.ametys.plugins.repository.RemovableAmetysObject; 052import org.ametys.plugins.repository.jcr.NameHelper; 053import org.ametys.plugins.repository.lock.LockableAmetysObject; 054import org.ametys.plugins.repository.query.expression.AndExpression; 055import org.ametys.plugins.repository.query.expression.Expression; 056import org.ametys.plugins.repository.query.expression.Expression.Operator; 057import org.ametys.plugins.repository.query.expression.StringExpression; 058import org.ametys.plugins.workflow.AbstractWorkflowComponent; 059import org.ametys.plugins.workflow.component.CheckRightsCondition; 060import org.ametys.runtime.plugin.component.AbstractLogEnabled; 061 062import com.opensymphony.workflow.WorkflowException; 063 064/** 065 * Copy updater to update the micro skills on a macro skill and the macro skills on a {@link Program}. 066 */ 067public class SkillsCopyUpdater extends AbstractLogEnabled implements CopyCatalogUpdater, Serviceable 068{ 069 private static final List<String> __SKILLS_IGNORED_ATTRIBUTES = List.of("parentMacroSkill", "microSkills", "parentProgram", "catalog"); 070 071 /** The ametys object resolver */ 072 protected AmetysObjectResolver _resolver; 073 /** The observation manager */ 074 protected ObservationManager _observationManager; 075 /** The current user provider */ 076 protected CurrentUserProvider _currentUserProvider; 077 /** The content workflow helper */ 078 protected ContentWorkflowHelper _contentWorkflowHelper; 079 080 public void service(ServiceManager manager) throws ServiceException 081 { 082 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 083 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 084 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 085 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 086 } 087 088 public void updateContents(String initialCatalogName, String newCatalogName, Map<Content, Content> copiedContents, Content targetParentContent) 089 { 090 // Do Nothing 091 } 092 093 public List<Content> getAdditionalContents(String catalogName) 094 { 095 List<Content> results = new ArrayList<>(); 096 097 results.addAll(_getContents(catalogName, SkillEditionFunction.MICRO_SKILL_TYPE)); 098 results.addAll(_getContents(catalogName, SkillEditionFunction.MACRO_SKILL_TYPE)); 099 100 return results; 101 } 102 103 public void copyAdditionalContents(String initialCatalogName, String newCatalogName, Map<Content, Content> copiedContents) 104 { 105 // Only copy the skills if they are enabled 106 if (!ODFSkillsHelper.isSkillsEnabled()) 107 { 108 return; 109 } 110 111 // Get the skills of the catalog to copy 112 List<DefaultContent> microSkillsToCopy = _getContents(initialCatalogName, SkillEditionFunction.MICRO_SKILL_TYPE); 113 List<DefaultContent> macroSkillsToCopy = _getContents(initialCatalogName, SkillEditionFunction.MACRO_SKILL_TYPE); 114 115 // Copy the micro skills in the new catalog 116 Map<String, Content> copiedMicroSkills = _copyMicroSkills(microSkillsToCopy, newCatalogName); 117 // Copy the macro skills in the new catalog and update the links to the copied micro skills 118 Map<String, Content> copiedMacroSkills = _copyMacroSkills(macroSkillsToCopy, newCatalogName, copiedMicroSkills); 119 120 // Update the links to the copied macro skills in programs 121 _updateContentsAfterSkillsCreation(newCatalogName, copiedContents, copiedMacroSkills); 122 } 123 124 private Map<String, Content> _copyMacroSkills(List<DefaultContent> skills, String newCatalogName, Map<String, Content> copiedMicroSkills) 125 { 126 return _copySkills(skills, SkillEditionFunction.MACRO_SKILL_TYPE, newCatalogName, copiedMicroSkills); 127 } 128 129 private Map<String, Content> _copyMicroSkills(List<DefaultContent> skills, String newCatalogName) 130 { 131 return _copySkills(skills, SkillEditionFunction.MICRO_SKILL_TYPE, newCatalogName, Map.of()); 132 } 133 134 private Map<String, Content> _copySkills(List<DefaultContent> skills, String contentType, String newCatalogName, Map<String, Content> copiedMicroSkills) 135 { 136 Map<String, Content> copiedSkills = new HashMap<>(); 137 for (DefaultContent skill : skills) 138 { 139 // Log if the targeted catalog already contains the skill 140 if (_skillExists(skill, contentType, newCatalogName)) 141 { 142 getLogger().info("A skill already exists with the same code, catalog and language [{}, {}, {}]", skill.getValue("code"), newCatalogName, skill.getLanguage()); 143 } 144 // Copy the skill in the targeted catalog 145 else 146 { 147 try 148 { 149 ModifiableContent newSkill = _createSkill(skill, newCatalogName); 150 151 // If the skill is a macroSkill and has microSkills values, update the linkes to point to the new ones 152 if (contentType.equals(SkillEditionFunction.MACRO_SKILL_TYPE) && skill.hasValue("microSkills")) 153 { 154 // If the content is a macroSkill and has microSkills, link the new ones that where previously copied 155 Map<String, Object> values = new HashMap<>(); 156 ContentValue[] previousMicroSkills = skill.getValue("microSkills"); 157 List<Content> microSkills = Arrays.asList(previousMicroSkills) 158 .stream() 159 .filter(Objects::nonNull) 160 .map(microSkill -> copiedMicroSkills.get(microSkill.getContentId())) 161 .toList(); 162 163 values.put("microSkills", microSkills); 164 try 165 { 166 _contentWorkflowHelper.editContent((ModifiableDefaultContent) newSkill, values, 2); 167 } 168 catch (Exception e) 169 { 170 // Log and rollback 171 getLogger().error("Impossible to update skill '{}' ({}) while creating the catalog {}", newSkill.getTitle(), newSkill.getId(), newCatalogName); 172 173 _deleteContent(newSkill); 174 } 175 } 176 177 // If the skill could be created, add it to the copied skills 178 if (newSkill != null) 179 { 180 copiedSkills.put(skill.getId(), newSkill); 181 } 182 } 183 catch (AmetysRepositoryException e) 184 { 185 getLogger().error("Impossible to create the skill '{}' ({}) while creating the catalog {}", skill.getTitle(), skill.getId(), newCatalogName, e); 186 } 187 } 188 } 189 190 return copiedSkills; 191 } 192 193 private ModifiableContent _createSkill(DefaultContent skill, String newCatalogName) throws AmetysRepositoryException 194 { 195 // Create the skill in the new catalog 196 ModifiableContent newSkill = skill.copyTo((ModifiableTraversableAmetysObject) skill.getParent(), NameHelper.filterName(skill.getTitle())); 197 198 // Remove the attributes that need to be updated 199 for (String data : newSkill.getDataNames()) 200 { 201 // Remove the attribute that can't be copied 202 if (__SKILLS_IGNORED_ATTRIBUTES.contains(data)) 203 { 204 newSkill.removeValue(data); 205 } 206 } 207 // Set the new catalog 208 newSkill.setValue("catalog", newCatalogName); 209 newSkill.saveChanges(); 210 211 return newSkill; 212 } 213 214 private void _updateContentsAfterSkillsCreation(String newCatalogName, Map<Content, Content> copiedContents, Map<String, Content> copiedMacroSkills) 215 { 216 // For every copied program, update its links from the original macro skills to the copied macro skills 217 for (Content copiedContent : copiedContents.values()) 218 { 219 try 220 { 221 if (copiedContent instanceof Program program) 222 { 223 ContentValue[] programSkills = program.getValue("skills"); 224 // Remove the skills without triggering the observer that would delete the skills 225 program.removeValue("skills"); 226 program.saveChanges(); 227 228 // If the program has skills, update the links to target the copied macro skills 229 if (programSkills != null) 230 { 231 Content[] skills = _getCopiedSkills(programSkills, copiedMacroSkills); 232 233 for (Content skill : skills) 234 { 235 ModifiableContent skillModifiable = (ModifiableContent) skill; 236 // Set the parent program of the copied skills 237 skillModifiable.synchronizeValues(Map.of("parentProgram", program.getId())); 238 skillModifiable.saveChanges(); 239 240 // To update the version and indexation 241 _editContent(skillModifiable); 242 } 243 244 program.saveChanges(); 245 } 246 247 ContentValue[] programTransversalSkills = program.getValue("transversalSkills"); 248 // If the program has transversal skills linked, update the link to target the copied macro skills 249 if (programTransversalSkills != null) 250 { 251 Content[] transversalSkills = _getCopiedSkills(programTransversalSkills, copiedMacroSkills); 252 program.setValue("transversalSkills", transversalSkills); 253 program.saveChanges(); 254 } 255 } 256 } 257 catch (Exception e) 258 { 259 getLogger().error("An error occurred while copying the program '{}' in the new catalog '{}'", copiedContent.getId(), newCatalogName, e); 260 } 261 } 262 } 263 264 private Content[] _getCopiedSkills(ContentValue[] originalSkills, Map<String, Content> copiedMacroSkills) 265 { 266 return Arrays.asList(originalSkills) 267 .stream() 268 // Keep the former skill if the copy is not found 269 .map(originalSkill -> copiedMacroSkills.getOrDefault(originalSkill.getContentId(), originalSkill.getContent())) 270 .toArray(Content[]::new); 271 } 272 273 private boolean _skillExists(Content skill, String contentType, String newCatalogName) 274 { 275 return _getContents(newCatalogName, contentType, skill).findAny().isPresent(); 276 } 277 278 private <T extends Content> List<T> _getContents(String catalogName, String contentType) 279 { 280 return this.<T>_getContents(catalogName, contentType, null).toList(); 281 } 282 283 private <T extends Content> Stream<T> _getContents(String catalogName, String contentType, Content skill) 284 { 285 List<Expression> exprs = new ArrayList<>(); 286 exprs.add(new ContentTypeExpression(Operator.EQ, contentType)); 287 exprs.add(new StringExpression("catalog", Operator.EQ, catalogName)); 288 if (skill != null) 289 { 290 exprs.add(new StringExpression("code", Operator.EQ, skill.getValue("code"))); 291 } 292 Expression expression = new AndExpression(exprs.toArray(Expression[]::new)); 293 294 String query = ContentQueryHelper.getContentXPathQuery(expression); 295 return _resolver.<T>query(query).stream(); 296 } 297 298 private void _deleteContent(Content content) 299 { 300 Map<String, Object> eventParams = new HashMap<>(); 301 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 302 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 303 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 304 ModifiableAmetysObject parent = content.getParent(); 305 306 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams)); 307 308 // Remove the content. 309 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 310 if (lockedContent.isLocked()) 311 { 312 lockedContent.unlock(); 313 } 314 315 ((RemovableAmetysObject) content).remove(); 316 317 parent.saveChanges(); 318 319 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams)); 320 } 321 322 private void _editContent(Content content) throws AmetysRepositoryException, WorkflowException 323 { 324 Map<String, Object> parameters = new HashMap<>(); 325 parameters.put(EditContentFunction.QUIT, true); 326 327 Map<String, Object> inputs = new HashMap<>(); 328 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters); 329 // Force because we don't want to control user rights with this operation 330 inputs.put(CheckRightsCondition.FORCE, true); 331 332 _contentWorkflowHelper.doAction((WorkflowAwareContent) content, 222, inputs); 333 } 334}