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.plugins.odforientation;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Map.Entry;
024
025import javax.xml.transform.TransformerFactory;
026import javax.xml.transform.dom.DOMResult;
027import javax.xml.transform.sax.SAXTransformerFactory;
028import javax.xml.transform.sax.TransformerHandler;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.XMLUtils;
035import org.apache.commons.lang3.LocaleUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.w3c.dom.Node;
038import org.w3c.dom.NodeList;
039
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.ModifiableContent;
043import org.ametys.cms.transformation.xslt.AmetysXSLTHelper;
044import org.ametys.core.util.dom.AmetysNodeList;
045import org.ametys.odf.enumeration.OdfReferenceTableEntry;
046import org.ametys.odf.xslt.OdfReferenceTableEntryElement;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.plugins.repository.data.holder.group.Repeater;
049import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
050
051/**
052 * XSLT helper for dataviz in ODF documents.
053 */
054public class DatavizXSLTHelper implements Serviceable
055{
056    private static AmetysObjectResolver _resolver;
057    
058    @Override
059    public void service(ServiceManager smanager) throws ServiceException
060    {
061        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
062    }
063    
064    /**
065     * Get the career outcome domain entry for a given career outcome content id.
066     * @param contentId The career outcome content id
067     * @return The career outcome domain entry or null if not found
068     */
069    public Node getCareerOutcomeDomain(String contentId)
070    {
071        Content content = _resolver.resolveById(contentId);
072        
073        Content domain = _getDomainFromOutcome(content);
074        if (domain != null)
075        {
076            OdfReferenceTableEntry entry = new OdfReferenceTableEntry(domain);
077            return new OdfReferenceTableEntryElement(entry, AmetysXSLTHelper.lang());
078        }
079        
080        return null;
081    }
082    
083    /**
084     * Group career outcomes by their domain for a given content id.
085     * @param contentId The content id containing the career outcomes distribution
086     * @return The distribution grouped by domain as an AmetysNodeList or null if not found
087     */
088    public static AmetysNodeList groupOutcomesByDomain(String contentId)
089    {
090        Content content = _resolver.resolveById(contentId);
091        
092        if (!content.hasDefinition("careerOutcomes/distribution"))
093        {
094            return null;
095        }
096        
097        Map<ContentValue, Map<ContentValue, Double>> outcomesByDomain = new HashMap<>();
098        
099        Repeater outcomesDistribution = content.getValue("careerOutcomes/distribution");
100        if (outcomesDistribution != null)
101        {
102            for (RepeaterEntry entry : outcomesDistribution.getEntries())
103            {
104                ContentValue careerOutcome = entry.getValue("careerOutcome");
105                ContentValue domain = _getDomainFromOutcome(careerOutcome);
106                Double weight = entry.getValue("weight");
107                
108                Map<ContentValue, Double> orDefault = outcomesByDomain.getOrDefault(domain, new HashMap<>());
109                weight += orDefault.getOrDefault(careerOutcome, 0.0);
110                orDefault.put(careerOutcome, weight);
111                
112                outcomesByDomain.put(domain, orDefault);
113            }
114        }
115        
116        return _getDistribution(outcomesByDomain);
117    }
118    
119    private static Content _getDomainFromOutcome(Content careerOutcome)
120    {
121        ContentValue parent = careerOutcome.getValue("parent");
122        if (parent != null)
123        {
124            if (parent.getValue("parent") != null)
125            {
126                return parent.getContent(); // outcome is a subdomain (level 3), return its parent domain (level 2)
127            }
128            else
129            {
130                return careerOutcome; // outcome is a domain (level 2), return itself
131            }
132        }
133        
134        return careerOutcome; // outcome is already a domain of level 1, return itself
135    }
136    
137    private static ContentValue _getDomainFromOutcome(ContentValue careerOutcome)
138    {
139        ContentValue parent = careerOutcome.getValue("parent");
140        if (parent != null)
141        {
142            if (parent.getValue("parent") != null)
143            {
144                return parent; // outcome is a subdomain (level 3), return its parent domain (level 2)
145            }
146            else
147            {
148                return careerOutcome; // outcome is a domain (level 2), return itself
149            }
150        }
151        
152        return careerOutcome; // outcome is already a domain of level 1, return itself
153    }
154    
155    private static AmetysNodeList _getDistribution(Map<ContentValue, Map<ContentValue, Double>> distributionByDomain)
156    {
157        try
158        {
159            String lang = AmetysXSLTHelper.lang();
160            Locale locale = LocaleUtils.toLocale(lang);
161            SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
162            TransformerHandler th = saxTransformerFactory.newTransformerHandler();
163            
164            DOMResult result = new DOMResult();
165            th.setResult(result);
166            
167            th.startDocument();
168            XMLUtils.startElement(th, "distribution");
169            for (Entry<ContentValue, Map<ContentValue, Double>> entry : distributionByDomain.entrySet())
170            {
171                Content domain = entry.getKey().getContent();
172                AttributesImpl domainAttrs = new AttributesImpl();
173                domainAttrs.addCDATAAttribute("id", domain.getId());
174                domainAttrs.addCDATAAttribute("code", domain.getValueOrDefault("code", StringUtils.EMPTY));
175                domainAttrs.addCDATAAttribute("title", domain.getTitle(locale));
176                XMLUtils.startElement(th, "domain", domainAttrs);
177                
178                for (Entry<ContentValue, Double> outcomeEntry : entry.getValue().entrySet())
179                {
180                    ModifiableContent outcome = outcomeEntry.getKey().getContent();
181                    AttributesImpl attrs = new AttributesImpl();
182                    attrs.addCDATAAttribute("id", outcome.getId());
183                    attrs.addCDATAAttribute("code", outcome.getValueOrDefault("code", StringUtils.EMPTY));
184                    attrs.addCDATAAttribute("title", outcome.getTitle(locale));
185                    XMLUtils.startElement(th, "careerOutcome", attrs);
186                    XMLUtils.createElement(th, "title", outcome.getTitle(locale));
187                    XMLUtils.createElement(th, "weight", String.valueOf(outcomeEntry.getValue()));
188                    XMLUtils.endElement(th, "careerOutcome");
189                }
190                XMLUtils.endElement(th, "domain");
191            }
192            XMLUtils.endElement(th, "distribution");
193            th.endDocument();
194            
195            List<Node> values = new ArrayList<>();
196            
197            // #getChildNodes() returns a NodeList that contains the value(s) saxed
198            // we cannot returns directly this NodeList because saxed values should be wrapped into a <value> tag.
199            NodeList childNodes = result.getNode().getFirstChild().getChildNodes();
200            for (int i = 0; i < childNodes.getLength(); i++)
201            {
202                Node n = childNodes.item(i);
203                values.add(n);
204            }
205            
206            return new AmetysNodeList(values);
207        }
208        catch (Exception e)
209        {
210            return null;
211        }
212    }
213
214}