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}