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 MACRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.macroSkill";
054    /** The content type of micro skills */
055    public static final String MICRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.microSkill";
056    /** Content name prefix for skills */
057    public static final String CONTENT_NAME_PREFIX = "skill-";
058    /** Constant for storing the catalog name to use into the transient variables map. */
059    public static final String CONTENT_TRANSVERSAL_KEY = SkillEditionFunction.class.getName() + "$transversal";
060    
061    /** Constant for storing the catalog name to use into the transient variables map. */
062    public static final Set<Integer> CONTENT_CREATION_ACTION_IDS = Set.of(0, 1);
063    
064    /** Ametys object resolver available to subclasses. */
065    protected AmetysObjectResolver _resolver;
066    /** The content types helper */
067    protected ContentTypesHelper _contentTypesHelper;
068    
069    @Override
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        super.service(manager);
073        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
074        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
075    }
076    
077    @Override
078    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
079    {
080        WorkflowAwareContent content = getContent(transientVars);
081        
082        // If we are creating the skill, add the initial values
083        if (_contentTypesHelper.isInstanceOf(content, ASBTRACT_SKILL_TYPE) && CONTENT_CREATION_ACTION_IDS.contains((int) transientVars.get("actionId")))
084        {
085            if (_contentTypesHelper.isInstanceOf(content, MACRO_SKILL_TYPE))
086            {
087                _updateMacroSkill((ModifiableContent) content, transientVars);
088            }
089            else if (_contentTypesHelper.isInstanceOf(content, MICRO_SKILL_TYPE))
090            {
091                _updateMicroSkill((ModifiableContent) content, transientVars);
092            }
093        }
094    }
095    
096    private void _updateSkill(ModifiableContent skillContent, Map transientVars, Optional<String> parentId)
097    {
098        Optional<String> catalog = Optional.empty();
099        if (parentId.isPresent())
100        {
101            catalog = _getParentCatalog(parentId.get());
102        }
103        
104        if (catalog.isEmpty())
105        {
106            // Try to retrieve the catalog from transient vars or initial value supplier
107            catalog = this.<String>_getValueFromTransientVars(transientVars, AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY)
108                // Used to get the catalog if the skill item is created by java code, like for the csv import.
109                .or(() -> this.<String>_getValueFromInitialValueSupplier(transientVars, List.of("catalog")))
110                .or(() -> this.<String>_getValueFromParameters(transientVars, "catalog"));
111        }
112        
113        /* Set the catalog */
114        if (catalog.isPresent())
115        {
116            // Set the catalog if value is present
117            skillContent.setValue("catalog", catalog.get());
118        }
119        
120        /* Set the code */
121        
122        // Used to get the code if the skill is created by java code, like for the csv import.
123        _getValueFromInitialValueSupplier(transientVars, List.of("code"))
124            // Set the code if value is present
125            .ifPresent(value -> skillContent.setValue("code", value));
126        
127        // Generate a code if empty
128        String code = skillContent.getValue("code");
129        if (StringUtils.isEmpty(code))
130        {
131            skillContent.setValue("code", org.ametys.core.util.StringUtils.generateKey().toUpperCase());
132        }
133        
134        skillContent.saveChanges();
135    }
136    
137    private void _updateMacroSkill(ModifiableContent skillContent, Map transientVars)
138    {
139        
140        // Try to get the parent program from parent context value transient var
141        Optional<String> optionalParentId = Optional.ofNullable((String) transientVars.get(CreateContentFunction.PARENT_CONTEXT_VALUE));
142    
143        // Try to get the transversal attribute from initial value supplier
144        Optional<Object> optionalTransversal = _getValueFromTransientVars(transientVars, CONTENT_TRANSVERSAL_KEY)
145                // Used to get the transversal if the skill item is created by java code, like for the csv import.
146                .or(() -> _getValueFromInitialValueSupplier(transientVars, List.of("transversal")))
147                .or(() -> _getValueFromParameters(transientVars, "transversal"));
148        
149        /* Set the parent */
150        if (optionalParentId.isPresent())
151        {
152            String parentId = optionalParentId.get();
153            // Set the parent program if value is present and synchronize it
154            skillContent.synchronizeValues(Map.of("parentProgram", parentId));
155            skillContent.setValue("transversal", false);
156        }
157        else if (optionalTransversal.isPresent())
158        {
159            Object transversalObj = optionalTransversal.get();
160            boolean transversal = transversalObj instanceof String ? Boolean.valueOf((String) transversalObj) : (Boolean) transversalObj;
161            
162            skillContent.setValue("transversal", transversal);
163        }
164        else
165        {
166            // Defaults to true (example when importing skills via CSV)
167            skillContent.setValue("transversal", true);
168        }
169        
170        _updateSkill(skillContent, transientVars, optionalParentId);
171    }
172    
173    private void _updateMicroSkill(ModifiableContent skillContent, Map transientVars)
174    {
175     // Try to get the parent program from parent context value transient var
176        Optional<String> optionalParentId = Optional.ofNullable((String) transientVars.get(CreateContentFunction.PARENT_CONTEXT_VALUE));
177    
178        /* Set the parent */
179        if (optionalParentId.isPresent())
180        {
181            String parentId = optionalParentId.get();
182            // Set the parent macro skill if value is present and synchronize it
183            skillContent.synchronizeValues(Map.of("parentMacroSkill", parentId));
184        }
185        
186        _updateSkill(skillContent, transientVars, optionalParentId);
187    }
188    
189    private Optional<String> _getParentCatalog(String parentId)
190    {
191        if (StringUtils.isNotBlank(parentId))
192        {
193            Object parent = _resolver.resolveById(parentId);
194            if (parent != null && parent instanceof Content parentContent && parentContent.hasValue("catalog"))
195            {
196                return Optional.of(parentContent.getValue("catalog"));
197            }
198        }
199        
200        return Optional.empty();
201    }
202    
203    @Override
204    public I18nizableText getLabel()
205    {
206        return new I18nizableText("plugin.odf", "PLUGINS_ODF_CREATE_SKILL_CONTENT_FUNCTION_LABEL");
207    }
208    
209    @SuppressWarnings("unchecked")
210    private <T> Optional<T> _getValueFromTransientVars(Map transientVars, String attributeName)
211    {
212        return Optional.ofNullable((T) transientVars.get(attributeName))
213                .filter(Objects::nonNull);
214    }
215    
216    @SuppressWarnings("unchecked")
217    private <T> Optional<T> _getValueFromInitialValueSupplier(Map transientVars, List<String> attributePath)
218    {
219        return Optional.ofNullable(transientVars.get(CreateContentFunction.INITIAL_VALUE_SUPPLIER))
220                .map(Function.class::cast)
221                .map(function -> function.apply(attributePath))
222                .filter(Objects::nonNull)
223                .map(value -> (T) value);
224    }
225    
226    @SuppressWarnings("unchecked")
227    private <T> Optional<T> _getValueFromParameters(Map transientVars, String attributeName)
228    {
229        Object objectParameters = transientVars.get(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY);
230        
231        if (objectParameters != null && objectParameters instanceof Map parameters)
232        {
233            Object values = parameters.get(EditContentFunction.FORM_RAW_VALUES);
234            if (values != null && values instanceof Map valuesMap)
235            {
236                return Optional.ofNullable((T) valuesMap.get(EditContentFunction.FORM_ELEMENTS_PREFIX + attributeName))
237                        .filter(Objects::nonNull);
238            }
239        }
240        
241        return Optional.empty();
242    }
243}