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