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