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.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.stream.Stream; 023 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.avalon.framework.service.Serviceable; 027 028import org.ametys.cms.contenttype.ContentTypesHelper; 029import org.ametys.cms.repository.Content; 030import org.ametys.cms.repository.ContentQueryHelper; 031import org.ametys.cms.repository.ContentTypeExpression; 032import org.ametys.cms.repository.DefaultContent; 033import org.ametys.cms.repository.ModifiableContent; 034import org.ametys.odf.course.Course; 035import org.ametys.odf.program.Program; 036import org.ametys.odf.skill.ODFSkillsHelper; 037import org.ametys.odf.skill.workflow.SkillEditionFunction; 038import org.ametys.plugins.repository.AmetysObjectResolver; 039import org.ametys.plugins.repository.AmetysRepositoryException; 040import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 041import org.ametys.plugins.repository.jcr.NameHelper; 042import org.ametys.plugins.repository.query.expression.AndExpression; 043import org.ametys.plugins.repository.query.expression.Expression.Operator; 044import org.ametys.plugins.repository.query.expression.StringExpression; 045import org.ametys.runtime.model.ModelItem; 046 047/** 048 * Copy updater to update the micro skills on a macro skill and the macro skills on a {@link Program} and (@link Course}. 049 */ 050public class SkillsCopyUpdater extends AbstractCopyCatalogUpdater implements Serviceable 051{ 052 /** The ametys object resolver */ 053 protected AmetysObjectResolver _resolver; 054 /** The content types helper */ 055 protected ContentTypesHelper _contentTypesHelper; 056 057 public void service(ServiceManager manager) throws ServiceException 058 { 059 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 060 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 061 } 062 063 public List<Content> getAdditionalContents(String catalogName) 064 { 065 List<Content> results = new ArrayList<>(); 066 results.addAll(_getContents(catalogName, SkillEditionFunction.MICRO_SKILL_TYPE)); 067 results.addAll(_getContents(catalogName, SkillEditionFunction.TRANSVERSAL_MACRO_SKILL_TYPE)); 068 results.addAll(_getContents(catalogName, SkillEditionFunction.DISCIPLINARY_MACRO_SKILL_TYPE)); 069 return results; 070 } 071 072 public Map<Content, Content> copyAdditionalContents(String initialCatalogName, String newCatalogName) 073 { 074 // Only copy the skills if they are enabled 075 if (!ODFSkillsHelper.isSkillsEnabled()) 076 { 077 return Map.of(); 078 } 079 080 Map<Content, Content> copiedContents = new HashMap<>(); 081 copiedContents.putAll(_copySkills(_getContents(initialCatalogName, SkillEditionFunction.MICRO_SKILL_TYPE), newCatalogName)); 082 copiedContents.putAll(_copySkills(_getContents(initialCatalogName, SkillEditionFunction.TRANSVERSAL_MACRO_SKILL_TYPE), newCatalogName)); 083 copiedContents.putAll(_copySkills(_getContents(initialCatalogName, SkillEditionFunction.DISCIPLINARY_MACRO_SKILL_TYPE), newCatalogName)); 084 return copiedContents; 085 } 086 087 public void updateContents(String initialCatalogName, String newCatalogName, Map<Content, Content> copiedContents, Content targetParentContent) 088 { 089 // For every copied program, update its links from the original macro skills to the copied macro skills 090 for (Content copiedContent : copiedContents.values()) 091 { 092 try 093 { 094 if (copiedContent instanceof Program program) 095 { 096 if (ODFSkillsHelper.isSkillsEnabled()) 097 { 098 _updateContentAttribute(program, Program.SKILLS, copiedContents); 099 _updateContentAttribute(program, Program.TRANSVERSAL_SKILLS, copiedContents); 100 _updateContentAttribute(program, Program.BLOCKING_SKILLS, copiedContents); 101 program.saveChanges(); 102 } 103 } 104 else if (copiedContent instanceof Course course) 105 { 106 if (ODFSkillsHelper.isSkillsEnabled()) 107 { 108 _updateContentAttribute(course, Course.ACQUIRED_MICRO_SKILLS + ModelItem.ITEM_PATH_SEPARATOR + Course.ACQUIRED_MICRO_SKILLS_SKILLS, copiedContents); 109 _updateContentAttribute(course, Course.ACQUIRED_MICRO_SKILLS + ModelItem.ITEM_PATH_SEPARATOR + Course.ACQUIRED_MICRO_SKILLS_PROGRAM, copiedContents); 110 course.saveChanges(); 111 } 112 } 113 else if (copiedContent instanceof ModifiableContent modifiableCopiedContent) 114 { 115 if (_contentTypesHelper.isInstanceOf(copiedContent, SkillEditionFunction.ASBTRACT_MACRO_SKILL_TYPE)) 116 { 117 if (ODFSkillsHelper.isSkillsEnabled()) 118 { 119 _updateContentAttribute(modifiableCopiedContent, "parentProgram", copiedContents); 120 } 121 _updateContentAttribute(modifiableCopiedContent, "microSkills", copiedContents); 122 modifiableCopiedContent.saveChanges(); 123 } 124 else if (_contentTypesHelper.isInstanceOf(copiedContent, SkillEditionFunction.MICRO_SKILL_TYPE)) 125 { 126 _updateContentAttribute(modifiableCopiedContent, "parentMacroSkill", copiedContents); 127 modifiableCopiedContent.saveChanges(); 128 } 129 } 130 } 131 catch (Exception e) 132 { 133 getLogger().error("An error occurred while updating the content '{}'", copiedContent.getId(), e); 134 } 135 } 136 } 137 138 private Map<Content, Content> _copySkills(List<DefaultContent> skills, String newCatalogName) 139 { 140 Map<Content, Content> copiedSkills = new HashMap<>(); 141 for (DefaultContent skill : skills) 142 { 143 // Copy the skill in the targeted catalog 144 try 145 { 146 // Create the skill 147 ModifiableContent newSkill = skill.copyTo((ModifiableTraversableAmetysObject) skill.getParent(), NameHelper.filterName(skill.getTitle())); 148 149 // Set the new catalog 150 newSkill.setValue("catalog", newCatalogName); 151 newSkill.saveChanges(); 152 153 copiedSkills.put(skill, newSkill); 154 } 155 catch (AmetysRepositoryException e) 156 { 157 getLogger().error("Impossible to create the skill '{}' ({}) while creating the catalog {}", skill.getTitle(), skill.getId(), newCatalogName, e); 158 } 159 } 160 161 return copiedSkills; 162 } 163 164 private <T extends Content> List<T> _getContents(String catalogName, String contentType) 165 { 166 return this.<T>_getContents(catalogName, contentType, null).toList(); 167 } 168 169 private <T extends Content> Stream<T> _getContents(String catalogName, String contentType, Content skill) 170 { 171 AndExpression expression = new AndExpression(); 172 expression.add(new ContentTypeExpression(Operator.EQ, contentType)); 173 expression.add(new StringExpression("catalog", Operator.EQ, catalogName)); 174 if (skill != null) 175 { 176 expression.add(new StringExpression("code", Operator.EQ, skill.getValue("code"))); 177 } 178 179 String query = ContentQueryHelper.getContentXPathQuery(expression); 180 return _resolver.<T>query(query).stream(); 181 } 182}