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.skill.workflow;
017
018import java.util.List;
019import java.util.Map;
020import java.util.Objects;
021import java.util.Optional;
022import java.util.Set;
023import java.util.function.Function;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang3.StringUtils;
028
029import org.ametys.cms.contenttype.ContentTypesHelper;
030import org.ametys.cms.repository.Content;
031import org.ametys.cms.repository.ModifiableContent;
032import org.ametys.cms.repository.WorkflowAwareContent;
033import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
034import org.ametys.cms.workflow.CreateContentFunction;
035import org.ametys.cms.workflow.EditContentFunction;
036import org.ametys.odf.workflow.AbstractCreateODFContentFunction;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.workflow.AbstractWorkflowComponent;
039import org.ametys.plugins.workflow.EnhancedFunction;
040import org.ametys.runtime.i18n.I18nizableText;
041
042import com.opensymphony.module.propertyset.PropertySet;
043import com.opensymphony.workflow.WorkflowException;
044
045/**
046 * OSWorkflow function for creating a MacroSkill or MicroSkill content
047 */
048public class SkillEditionFunction extends AbstractContentWorkflowComponent implements EnhancedFunction
049{
050    /** The content type of macro skills */
051    public static final String ASBTRACT_SKILL_TYPE = "org.ametys.plugins.odf.Content.abstractSkill";
052    /** The content type of macro skills */
053    public static final String ASBTRACT_MACRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.abstractMacroSkill";
054    /** The content type of transversal macro skills */
055    public static final String TRANSVERSAL_MACRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.transversalMacroSkill";
056    /** The content type of program macro skills */
057    public static final String DISCIPLINARY_MACRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.disciplinaryMacroSkill";
058    /** The content type of micro skills */
059    public static final String MICRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.microSkill";
060    /** Content name prefix for skills */
061    public static final String CONTENT_NAME_PREFIX = "skill-";
062    /** Constant for storing the catalog name to use into the transient variables map. */
063    public static final String CONTENT_TRANSVERSAL_KEY = SkillEditionFunction.class.getName() + "$transversal";
064    
065    /** Constant for storing the catalog name to use into the transient variables map. */
066    public static final Set<Integer> CONTENT_CREATION_ACTION_IDS = Set.of(0, 1);
067    
068    /** Ametys object resolver available to subclasses. */
069    protected AmetysObjectResolver _resolver;
070    /** The content types helper */
071    protected ContentTypesHelper _contentTypesHelper;
072    
073    @Override
074    public void service(ServiceManager manager) throws ServiceException
075    {
076        super.service(manager);
077        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
078        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
079    }
080    
081    @Override
082    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
083    {
084        WorkflowAwareContent content = getContent(transientVars);
085        
086        // If we are creating the skill, add the initial values
087        if (_contentTypesHelper.isInstanceOf(content, ASBTRACT_SKILL_TYPE) && CONTENT_CREATION_ACTION_IDS.contains((int) transientVars.get("actionId")))
088        {
089            if (_contentTypesHelper.isInstanceOf(content, TRANSVERSAL_MACRO_SKILL_TYPE))
090            {
091                _updateSkill((ModifiableContent) content, transientVars, Optional.empty());
092            }
093            if (_contentTypesHelper.isInstanceOf(content, DISCIPLINARY_MACRO_SKILL_TYPE))
094            {
095                _updateSkillWithParent((ModifiableContent) content, transientVars, "parentProgram");
096            }
097            else if (_contentTypesHelper.isInstanceOf(content, MICRO_SKILL_TYPE))
098            {
099                _updateSkillWithParent((ModifiableContent) content, transientVars, "parentMacroSkill");
100            }
101        }
102    }
103    
104    private void _updateSkillWithParent(ModifiableContent skillContent, Map transientVars, String parentAttributeName)
105    {
106        // Try to get the parent program from parent context value transient var
107        Optional<String> optionalParentId = Optional.ofNullable((String) transientVars.get(CreateContentFunction.PARENT_CONTEXT_VALUE));
108    
109        Optional<String> catalog = Optional.empty();
110        /* Set the parent */
111        if (optionalParentId.isPresent())
112        {
113            String parentId = optionalParentId.get();
114            // Set the parent macro skill if value is present and synchronize it
115            skillContent.synchronizeValues(Map.of(parentAttributeName, parentId));
116            
117            catalog = _getParentCatalog(parentId);
118        }
119        
120        _updateSkill(skillContent, transientVars, catalog);
121    }
122    
123    private void _updateSkill(ModifiableContent skillContent, Map transientVars, Optional<String> catalog)
124    {
125        // Try to retrieve the catalog from transient vars or initial value supplier
126        Optional<String> catalogToUse = catalog.or(() -> this.<String>_getValueFromTransientVars(transientVars, AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY))
127                // Used to get the catalog if the skill item is created by java code, like for the csv import.
128                .or(() -> this.<String>_getValueFromInitialValueSupplier(transientVars, List.of("catalog")))
129                .or(() -> this.<String>_getValueFromParameters(transientVars, "catalog"));
130        
131        /* Set the catalog */
132        if (catalogToUse.isPresent())
133        {
134            // Set the catalog if value is present
135            skillContent.setValue("catalog", catalogToUse.get());
136        }
137        
138        /* Set the code */
139        // Used to get the code if the skill is created by java code, like for the csv import.
140        _getValueFromInitialValueSupplier(transientVars, List.of("code"))
141            // Set the code if value is present
142            .ifPresent(value -> skillContent.setValue("code", value));
143        
144        // Generate a code if empty
145        String code = skillContent.getValue("code");
146        if (StringUtils.isEmpty(code))
147        {
148            skillContent.setValue("code", org.ametys.core.util.StringUtils.generateKey().toUpperCase());
149        }
150        
151        skillContent.saveChanges();
152    }
153    
154    private Optional<String> _getParentCatalog(String parentId)
155    {
156        if (StringUtils.isNotBlank(parentId))
157        {
158            Object parent = _resolver.resolveById(parentId);
159            if (parent != null && parent instanceof Content parentContent && parentContent.hasValue("catalog"))
160            {
161                return Optional.of(parentContent.getValue("catalog"));
162            }
163        }
164        
165        return Optional.empty();
166    }
167    
168    @Override
169    public I18nizableText getLabel()
170    {
171        return new I18nizableText("plugin.odf", "PLUGINS_ODF_CREATE_SKILL_CONTENT_FUNCTION_LABEL");
172    }
173    
174    @SuppressWarnings("unchecked")
175    private <T> Optional<T> _getValueFromTransientVars(Map transientVars, String attributeName)
176    {
177        return Optional.ofNullable((T) transientVars.get(attributeName))
178                .filter(Objects::nonNull);
179    }
180    
181    @SuppressWarnings("unchecked")
182    private <T> Optional<T> _getValueFromInitialValueSupplier(Map transientVars, List<String> attributePath)
183    {
184        return Optional.ofNullable(transientVars.get(CreateContentFunction.INITIAL_VALUE_SUPPLIER))
185                .map(Function.class::cast)
186                .map(function -> function.apply(attributePath))
187                .filter(Objects::nonNull)
188                .map(value -> (T) value);
189    }
190    
191    @SuppressWarnings("unchecked")
192    private <T> Optional<T> _getValueFromParameters(Map transientVars, String attributeName)
193    {
194        Object objectParameters = transientVars.get(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY);
195        
196        if (objectParameters != null && objectParameters instanceof Map parameters)
197        {
198            Object values = parameters.get(EditContentFunction.FORM_RAW_VALUES);
199            if (values != null && values instanceof Map valuesMap)
200            {
201                return Optional.ofNullable((T) valuesMap.get(EditContentFunction.FORM_ELEMENTS_PREFIX + attributeName))
202                        .filter(Objects::nonNull);
203            }
204        }
205        
206        return Optional.empty();
207    }
208}