001/* 002 * Copyright 2015 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.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.stream.Collectors; 025import java.util.stream.Stream; 026 027import javax.xml.transform.TransformerFactory; 028import javax.xml.transform.dom.DOMResult; 029import javax.xml.transform.sax.SAXTransformerFactory; 030import javax.xml.transform.sax.TransformerHandler; 031 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.xml.XMLUtils; 036import org.apache.commons.lang3.StringUtils; 037import org.w3c.dom.Element; 038import org.w3c.dom.Node; 039import org.w3c.dom.NodeList; 040 041import org.ametys.cms.repository.Content; 042import org.ametys.cms.transformation.xslt.AmetysXSLTHelper; 043import org.ametys.core.util.dom.AmetysNodeList; 044import org.ametys.core.util.dom.StringElement; 045import org.ametys.odf.course.Course; 046import org.ametys.odf.data.EducationalPath; 047import org.ametys.odf.enumeration.OdfReferenceTableEntry; 048import org.ametys.odf.enumeration.OdfReferenceTableHelper; 049import org.ametys.odf.orgunit.OrgUnit; 050import org.ametys.odf.orgunit.RootOrgUnitProvider; 051import org.ametys.odf.program.AbstractProgram; 052import org.ametys.odf.program.Program; 053import org.ametys.odf.program.SubProgram; 054import org.ametys.odf.xslt.OdfReferenceTableElement; 055import org.ametys.odf.xslt.ProgramElement; 056import org.ametys.odf.xslt.SubProgramElement; 057import org.ametys.plugins.repository.AmetysObjectResolver; 058import org.ametys.plugins.repository.AmetysRepositoryException; 059import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 060import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 061import org.ametys.runtime.config.Config; 062 063/** 064 * Helper component to be used from XSL stylesheets. 065 */ 066public class OdfXSLTHelper implements Serviceable 067{ 068 /** The ODF helper */ 069 protected static ODFHelper _odfHelper; 070 /** The ODF reference helper */ 071 protected static OdfReferenceTableHelper _odfRefTableHelper; 072 /** The Ametys resolver */ 073 protected static AmetysObjectResolver _ametysObjectResolver; 074 /** The orgunit root provider */ 075 protected static RootOrgUnitProvider _rootOrgUnitProvider; 076 077 @Override 078 public void service(ServiceManager smanager) throws ServiceException 079 { 080 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 081 _odfRefTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE); 082 _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 083 _rootOrgUnitProvider = (RootOrgUnitProvider) smanager.lookup(RootOrgUnitProvider.ROLE); 084 } 085 086 /** 087 * Get the label associated with the degree key 088 * @param cdmValue The code of degree 089 * @return The label of degree or code if not found 090 */ 091 public static String degreeLabel (String cdmValue) 092 { 093 return degreeLabel(cdmValue, Config.getInstance().getValue("odf.programs.lang")); 094 } 095 096 /** 097 * Get the code associated with the given reference table's entry 098 * @param tableRefEntryId The id of entry 099 * @return the code or <code>null</code> if not found 100 */ 101 public static String getCode (String tableRefEntryId) 102 { 103 try 104 { 105 Content content = _ametysObjectResolver.resolveById(tableRefEntryId); 106 return content.getValue(OdfReferenceTableEntry.CODE); 107 } 108 catch (AmetysRepositoryException e) 109 { 110 return null; 111 } 112 } 113 114 /** 115 * Get the id of reference table's entry 116 * @param tableRefId The id of content type 117 * @param code The code 118 * @return the id or <code>null</code> if not found 119 */ 120 public static String getEntryId (String tableRefId, String code) 121 { 122 OdfReferenceTableEntry entry = _odfRefTableHelper.getItemFromCode(tableRefId, code); 123 if (entry != null) 124 { 125 return entry.getId(); 126 } 127 return null; 128 } 129 130 /** 131 * Get the label associated with the degree key 132 * @param cdmValue The cdm value of degree 133 * @param lang The language 134 * @return The label of degree or empty string if not found 135 */ 136 public static String degreeLabel (String cdmValue, String lang) 137 { 138 return Optional 139 .ofNullable(_odfRefTableHelper.getItemFromCDM(OdfReferenceTableHelper.DEGREE, cdmValue)) 140 .map(degree -> degree.getLabel(lang)) 141 .orElse(StringUtils.EMPTY); 142 } 143 144 /** 145 * Get the whole structure of a subprogram, including the structure of child subprograms 146 * @param subprogramId The id of subprogram 147 * @return Node with the subprogram structure 148 */ 149 public static Node getSubProgramStructure (String subprogramId) 150 { 151 SubProgram subProgram = _ametysObjectResolver.resolveById(subprogramId); 152 return new SubProgramElement(subProgram, _odfHelper); 153 } 154 155 /** 156 * Get the structure of a subprogram, including the structure of child subprograms until the given depth 157 * @param subprogramId The id of subprogram 158 * @param depth Set a positive number to get structure of child subprograms until given depth. Set a negative number to get the whole structure recursively, including the structure of child subprograms. This parameter concerns only subprograms. 159 * @return Node with the subprogram structure 160 */ 161 public static Node getSubProgramStructure (String subprogramId, int depth) 162 { 163 SubProgram subProgram = _ametysObjectResolver.resolveById(subprogramId); 164 return new SubProgramElement(subProgram, depth, null, _odfHelper); 165 } 166 167 /** 168 * Get the parent program information 169 * @param subprogramId The id of subprogram 170 * @return a node for each program's information 171 */ 172 public static NodeList getParentProgram (String subprogramId) 173 { 174 return getParentProgramStructure(subprogramId, 0); 175 } 176 177 /** 178 * Get the certification label of a {@link AbstractProgram}. 179 * Returns null if the program is not certified. 180 * @param abstractProgramId the id of program or subprogram 181 * @return the certification label 182 */ 183 public static String getCertificationLabel(String abstractProgramId) 184 { 185 AbstractProgram abstractProgram = _ametysObjectResolver.resolveById(abstractProgramId); 186 if (abstractProgram.isCertified()) 187 { 188 String degreeId = null; 189 if (abstractProgram instanceof Program) 190 { 191 degreeId = ((Program) abstractProgram).getDegree(); 192 } 193 else if (abstractProgram instanceof SubProgram) 194 { 195 // Get degree from parent 196 Set<Program> rootPrograms = _odfHelper.getParentPrograms(abstractProgram); 197 if (rootPrograms.size() > 0) 198 { 199 degreeId = rootPrograms.iterator().next().getDegree(); 200 } 201 } 202 203 if (StringUtils.isNotEmpty(degreeId)) 204 { 205 Content degree = _ametysObjectResolver.resolveById(degreeId); 206 return degree.getValue("certificationLabel"); 207 } 208 } 209 210 return null; 211 } 212 213 /** 214 * Get the program information 215 * @param programId The id of program 216 * @return Node with the program's information 217 */ 218 public static Node getProgram (String programId) 219 { 220 return getProgramStructure(programId, 0); 221 } 222 223 /** 224 * Get the structure of a parent programs, including the structure of child subprograms until the given depth. 225 * @param subprogramId The id of subprogram 226 * @param depth Set a positive number to get structure of child subprograms until given depth. Set a negative number to get the whole structure recursively, including the structure of child subprograms. This parameter concerns only subprograms. 227 * @return a node for each program's structure 228 */ 229 public static NodeList getParentProgramStructure (String subprogramId, int depth) 230 { 231 List<ProgramElement> programs = new ArrayList<>(); 232 233 SubProgram subProgram = _ametysObjectResolver.resolveById(subprogramId); 234 235 Set<Program> rootPrograms = _odfHelper.getParentPrograms(subProgram); 236 if (rootPrograms.size() > 0) 237 { 238 programs.add(new ProgramElement(rootPrograms.iterator().next(), depth, null, _odfHelper)); 239 } 240 241 return new AmetysNodeList(programs); 242 } 243 244 /** 245 * Get the paths of a {@link ProgramItem} util the root program(s) 246 * Paths are built with content's title 247 * @param programItemId the id of program items 248 * @param separator The path separator 249 * @return the paths in program 250 */ 251 public static NodeList getProgramPaths(String programItemId, String separator) 252 { 253 ProgramItem programItem = _ametysObjectResolver.resolveById(programItemId); 254 List<String> paths = _odfHelper.getPaths(programItem, separator, p -> ((Content) p).getTitle(), false); 255 256 List<Element> result = paths.stream() 257 .map(path -> new StringElement("path", (Map<String, String>) null, path)) 258 .collect(Collectors.toList()); 259 260 return new AmetysNodeList(result); 261 } 262 263 /** 264 * Get the structure of a program until the given depth. 265 * @param programId The id of program 266 * @param depth Set a positive number to get structure of child subprograms until given depth. Set a negative number to get the whole structure recursively, including the structure of child subprograms. This parameter concerns only subprograms. 267 * @return Node with the program structure 268 */ 269 public static Node getProgramStructure (String programId, int depth) 270 { 271 Program program = _ametysObjectResolver.resolveById(programId); 272 return new ProgramElement(program, depth, null, _odfHelper); 273 } 274 275 /** 276 * Get the items of a reference table 277 * @param tableRefId the id of reference table 278 * @param lang the language to use for labels 279 * @return the items 280 */ 281 public static Node getTableRefItems(String tableRefId, String lang) 282 { 283 return getTableRefItems(tableRefId, lang, false, true); 284 } 285 286 /** 287 * Get the items of a reference table 288 * @param tableRefId the id of reference table 289 * @param lang the language to use for labels 290 * @param ordered true to sort items by 'order' attribute 291 * @return the items 292 */ 293 public static Node getTableRefItems(String tableRefId, String lang, boolean ordered) 294 { 295 return getTableRefItems(tableRefId, lang, ordered, true); 296 } 297 298 /** 299 * Get the items of a reference table 300 * @param tableRefId the id of reference table 301 * @param lang the language to use for labels 302 * @param ordered true to sort items by 'order' attribute 303 * @param includeArchived true to include archived items 304 * @return the items 305 */ 306 public static Node getTableRefItems(String tableRefId, String lang, boolean ordered, boolean includeArchived) 307 { 308 return new OdfReferenceTableElement(tableRefId, _odfRefTableHelper, lang, ordered, includeArchived); 309 } 310 311 /** 312 * Get the id of root orgunit 313 * @return The id of root 314 */ 315 public static String getRootOrgUnitId() 316 { 317 return _rootOrgUnitProvider.getRootId(); 318 } 319 320 /** 321 * Get the id of the first orgunit matching the given UAI code 322 * @param uaiCode the UAI code 323 * @return the id of orgunit or null if not found 324 */ 325 public static String getOrgUnitIdByUAICode(String uaiCode) 326 { 327 return Optional.ofNullable(uaiCode) 328 .map(_odfHelper::getOrgUnitByUAICode) 329 .map(OrgUnit::getId) 330 .orElse(null); 331 } 332 333 /** 334 * Get the more recent educational booklet for a {@link ProgramItem} 335 * @param programItemId the program item id 336 * @return the pdf as an ametys node list 337 */ 338 public static AmetysNodeList getEducationalBooklet(String programItemId) 339 { 340 try 341 { 342 Content programItem = _ametysObjectResolver.resolveById(programItemId); 343 if (programItem.hasValue(ProgramItem.EDUCATIONAL_BOOKLETS)) 344 { 345 ModelAwareRepeater repeater = programItem.getRepeater(ProgramItem.EDUCATIONAL_BOOKLETS); 346 ZonedDateTime dateToCompare = null; 347 ModelAwareRepeaterEntry entry = null; 348 for (ModelAwareRepeaterEntry repeaterEntry : repeater.getEntries()) 349 { 350 ZonedDateTime date = repeaterEntry.getValue("date"); 351 if (dateToCompare == null || date.isAfter(dateToCompare)) 352 { 353 dateToCompare = date; 354 entry = repeaterEntry; 355 } 356 } 357 358 if (entry != null) 359 { 360 SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); 361 TransformerHandler th = saxTransformerFactory.newTransformerHandler(); 362 363 DOMResult result = new DOMResult(); 364 th.setResult(result); 365 366 th.startDocument(); 367 XMLUtils.startElement(th, "value"); 368 if (entry.hasValue("pdf")) 369 { 370 programItem.dataToSAX(th, ProgramItem.EDUCATIONAL_BOOKLETS + "[" + entry.getPosition() + "]/pdf"); 371 } 372 XMLUtils.endElement(th, "value"); 373 th.endDocument(); 374 375 List<Node> values = new ArrayList<>(); 376 377 // #getChildNodes() returns a NodeList that contains the value(s) saxed 378 // we cannot returns directly this NodeList because saxed values should be wrapped into a <value> tag. 379 NodeList childNodes = result.getNode().getFirstChild().getChildNodes(); 380 for (int i = 0; i < childNodes.getLength(); i++) 381 { 382 Node n = childNodes.item(i); 383 values.add(n); 384 } 385 386 return new AmetysNodeList(values); 387 } 388 } 389 } 390 catch (Exception e) 391 { 392 return null; 393 } 394 395 return null; 396 } 397 398 /** 399 * Count the hours accumulation in the {@link ProgramItem} 400 * @param contentId The id of the {@link ProgramItem} 401 * @return The hours accumulation 402 */ 403 public static Double getCumulatedHours(String contentId) 404 { 405 return _odfHelper.getCumulatedHours(_ametysObjectResolver.<ProgramItem>resolveById(contentId)); 406 } 407 408 /** 409 * Get the ECTS value at the given educational path 410 * @param courseId The course id 411 * @param coursePathAsString The course path as string (semicolon separated). Can be a partial path. 412 * @return the ECTS value for the given educational path 413 */ 414 public static NodeList getEcts(String courseId, String coursePathAsString) 415 { 416 return getValueForPath(courseId, Course.ECTS_BY_PATH, coursePathAsString, Course.ECTS); 417 } 418 419 /** 420 * Get the attribute of a content at the given path for a given educational path 421 * @param programItemId The program item id 422 * @param dataPath The path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 423 * @param programItemPathAsString The program item path as string (semicolon separated). Can be a partial path 424 * @param defaultValuePath The data path of the default value in there is no specific value at the given educational path 425 * @return The value into a "value" node or null if an error occurred 426 */ 427 public static NodeList getValueForPath(String programItemId, String dataPath, String programItemPathAsString, String defaultValuePath) 428 { 429 ProgramItem programItem = _ametysObjectResolver.resolveById(programItemId); 430 431 String[] programItemIds = programItemPathAsString.split(EducationalPath.PATH_SEGMENT_SEPARATOR); 432 List<ProgramItem> programItemPath = Stream.of(programItemIds).map(id -> _ametysObjectResolver.<ProgramItem>resolveById(id)).toList(); 433 List<EducationalPath> educationaPaths = _odfHelper.getEducationPathFromPath(programItemPath); 434 435 int position = _odfHelper.getRepeaterEntryPositionForPath(programItem, dataPath, educationaPaths); 436 437 String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/"); 438 String attributeName = StringUtils.substringAfterLast(dataPath, "/"); 439 440 return AmetysXSLTHelper.contentAttribute(programItemId, position != -1 ? repeaterPath + "[" + position + "]/" + attributeName : defaultValuePath); 441 } 442 443 /** 444 * Convert a duration in minutes to a string representing the duration in hours. 445 * @param duree in minutes 446 * @return the duration in hours 447 */ 448 public static String minute2hour(int duree) 449 { 450 int h = duree / 60; 451 int m = duree % 60; 452 return String.format("%dh%02d", h, m); 453 } 454}