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