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}