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.Optional;
021import java.util.function.Function;
022
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.commons.lang3.ArrayUtils;
026import org.apache.commons.lang3.StringUtils;
027
028import org.ametys.cms.repository.Content;
029import org.ametys.cms.repository.ModifiableContent;
030import org.ametys.cms.repository.WorkflowAwareContent;
031import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
032import org.ametys.cms.workflow.CreateContentFunction;
033import org.ametys.odf.workflow.AbstractCreateODFContentFunction;
034import org.ametys.plugins.repository.AmetysObjectResolver;
035import org.ametys.plugins.workflow.EnhancedFunction;
036import org.ametys.runtime.i18n.I18nizableText;
037
038import com.opensymphony.module.propertyset.PropertySet;
039import com.opensymphony.workflow.WorkflowException;
040
041/**
042 * OSWorkflow function for creating a MacroSkill or MicroSkill content
043 */
044public class SkillEditionFunction extends AbstractContentWorkflowComponent implements EnhancedFunction
045{
046    /** The content type of macro skills */
047    public static final String MACRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.macroSkill";
048    /** The content type of micro skills */
049    public static final String MICRO_SKILL_TYPE = "org.ametys.plugins.odf.Content.microSkill";
050    /** Content name prefix for skills */
051    public static final String CONTENT_NAME_PREFIX = "skill-";
052    
053    /** Ametys object resolver available to subclasses. */
054    protected AmetysObjectResolver _resolver;
055    
056    @Override
057    public void service(ServiceManager manager) throws ServiceException
058    {
059        super.service(manager);
060        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
061    }
062    
063    @Override
064    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
065    {
066        WorkflowAwareContent content = getContent(transientVars);
067        
068        if (ArrayUtils.containsAny(content.getTypes(), SkillEditionFunction.MACRO_SKILL_TYPE, SkillEditionFunction.MICRO_SKILL_TYPE))
069        {
070            ModifiableContent skillContent = (ModifiableContent) content;
071            Optional<String> catalog = Optional.empty();
072            
073            Optional<String> parentProgram = _getParent(transientVars, skillContent);
074            
075            /* Set the parent program */
076            if (ArrayUtils.contains(skillContent.getTypes(), SkillEditionFunction.MACRO_SKILL_TYPE))
077            {
078                if (parentProgram.isPresent())
079                {
080                    String parentProgramId = parentProgram.get();
081                    // Set the parent program if value is present and synchronize it
082                    skillContent.synchronizeValues(Map.of("parentProgram", parentProgramId));
083                }
084            }
085            
086            catalog = _getCatalog(transientVars, parentProgram);
087            
088            /* Set the catalog */
089            if (catalog.isPresent())
090            {
091                // Set the catalog if value is present
092                skillContent.setValue("catalog", catalog.get());
093            }
094            
095            /* Set the code */
096            
097            // Used to get the code if the skill is created by java code, like for the csv import.
098            _getValueFromInitialValueSupplier(transientVars, List.of("code"))
099                // Set the code if value is present
100                .ifPresent(value -> skillContent.setValue("code", value));
101            
102            // Generate a code if empty
103            String code = content.getValue("code");
104            if (StringUtils.isEmpty(code))
105            {
106                skillContent.setValue("code", org.ametys.core.util.StringUtils.generateKey().toUpperCase());
107            }
108        }
109    }
110    
111    private Optional<String> _getCatalog(Map transientVars, Optional<String> parentId)
112    {
113        Optional<String> catalog = Optional.empty();
114        
115        // First try to retrieve the catalog from the parent if it is a microSkill
116        if (parentId.isPresent())
117        {
118            catalog = _getParentCatalog(parentId.get());
119        }
120        
121        // If there was no value in the parent or no parent, try to retrieve the catalog from transient vars or initial value supplier
122        if (catalog.isEmpty())
123        {
124            // Try to get the catalog from transient vars
125            catalog = _getValueFromTransientVars(transientVars, AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY)
126                    // Used to get the catalog if the skill item is created by java code, like for the csv import.
127                    .or(() -> _getValueFromInitialValueSupplier(transientVars, List.of("catalog")));
128        }
129        
130        return catalog;
131    }
132    
133    private Optional<String> _getParentCatalog(String parentId)
134    {
135        if (StringUtils.isNotBlank(parentId))
136        {
137            Object parent = _resolver.resolveById(parentId);
138            if (parent != null && parent instanceof Content parentContent && parentContent.hasValue("catalog"))
139            {
140                return Optional.of(parentContent.getValue("catalog"));
141            }
142        }
143        
144        return Optional.empty();
145    }
146    
147    private Optional<String> _getParent(Map transientVars, Content content)
148    {
149        // Try to get the parent program from parent context value transient var
150        Optional<String> parentId = Optional.ofNullable((String) transientVars.get(CreateContentFunction.PARENT_CONTEXT_VALUE));
151    
152        // If there was no value, search deeper in the transient vars
153        if (parentId.isEmpty() && ArrayUtils.contains(content.getTypes(), SkillEditionFunction.MACRO_SKILL_TYPE))
154        {
155            // Try to get the parent program from transient vars
156            parentId = _getValueFromTransientVars(transientVars, "parentProgram")
157                    // Used to get the parent program if the skill item is created by java code, like for the csv import.
158                    .or(() -> _getValueFromInitialValueSupplier(transientVars, List.of("parentProgram")));
159        }
160        
161        return parentId;
162    }
163    
164    @Override
165    public I18nizableText getLabel()
166    {
167        return new I18nizableText("plugin.odf", "PLUGINS_ODF_CREATE_SKILL_CONTENT_FUNCTION_LABEL");
168    }
169    
170    private Optional<String> _getValueFromTransientVars(Map transientVars, String attributeName)
171    {
172        return Optional.ofNullable(transientVars.get(attributeName))
173            .map(String.class::cast)
174            .filter(StringUtils::isNotEmpty);
175    }
176    
177    @SuppressWarnings("unchecked")
178    private Optional<String> _getValueFromInitialValueSupplier(Map transientVars, List<String> attributePath)
179    {
180        return Optional.ofNullable(transientVars.get(CreateContentFunction.INITIAL_VALUE_SUPPLIER))
181            .map(Function.class::cast)
182            .map(function -> function.apply(attributePath))
183            .map(String.class::cast)
184            .filter(StringUtils::isNotEmpty);
185    }
186}