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