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}