001/*
002 *  Copyright 2018 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.plugins.odfpilotage.helper;
017
018import java.time.LocalDate;
019import java.time.format.DateTimeFormatter;
020import java.util.ArrayList;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import javax.xml.transform.sax.TransformerHandler;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.cocoon.xml.AttributesImpl;
038import org.apache.cocoon.xml.XMLUtils;
039import org.apache.commons.collections.MapUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.slf4j.Logger;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.data.ContentDataHelper;
045import org.ametys.cms.data.ContentValue;
046import org.ametys.cms.data.type.ModelItemTypeConstants;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.repository.ContentTypeExpression;
049import org.ametys.cms.repository.LanguageExpression;
050import org.ametys.cms.repository.ModifiableDefaultContent;
051import org.ametys.odf.ODFHelper;
052import org.ametys.odf.ProgramItem;
053import org.ametys.odf.course.Course;
054import org.ametys.odf.enumeration.OdfReferenceTableEntry;
055import org.ametys.odf.enumeration.OdfReferenceTableHelper;
056import org.ametys.odf.orgunit.OrgUnit;
057import org.ametys.odf.orgunit.OrgUnitFactory;
058import org.ametys.odf.orgunit.RootOrgUnitProvider;
059import org.ametys.odf.program.AbstractProgram;
060import org.ametys.odf.program.Container;
061import org.ametys.odf.program.Program;
062import org.ametys.odf.program.ProgramFactory;
063import org.ametys.plugins.repository.AmetysObjectIterable;
064import org.ametys.plugins.repository.AmetysObjectIterator;
065import org.ametys.plugins.repository.AmetysObjectResolver;
066import org.ametys.plugins.repository.AmetysRepositoryException;
067import org.ametys.plugins.repository.UnknownAmetysObjectException;
068import org.ametys.plugins.repository.query.QueryHelper;
069import org.ametys.plugins.repository.query.expression.AndExpression;
070import org.ametys.plugins.repository.query.expression.Expression;
071import org.ametys.plugins.repository.query.expression.Expression.Operator;
072import org.ametys.runtime.config.Config;
073import org.ametys.plugins.repository.query.expression.OrExpression;
074import org.ametys.plugins.repository.query.expression.StringExpression;
075
076/**
077 * Helper for report creation.
078 */
079public class ReportHelper implements Component, Serviceable
080{
081    /** The avalon role */
082    public static final String ROLE = ReportHelper.class.getName();
083    
084    private static final String __READABLE_DF = "dd/MM/yyyy";
085    
086    /** The Ametys object resolver */
087    protected AmetysObjectResolver _resolver;
088    
089    /** The root orgunit provider */
090    protected RootOrgUnitProvider _rootOrgUnitProvider;
091    
092    /** The ODF helper */
093    protected ODFHelper _odfHelper;
094
095    /** The ODF enumeration helper */
096    protected OdfReferenceTableHelper _refTableHelper;
097    
098    @Override
099    public void service(ServiceManager manager) throws ServiceException
100    {
101        _rootOrgUnitProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
102        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
103        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
104        _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
105    }
106    
107    /**
108     * Get the current date to the following format : 'dd/MM/yyyy'
109     * @return The date as a {@link String}
110     */
111    public String getReadableCurrentDate()
112    {
113        return LocalDate.now().format(DateTimeFormatter.ofPattern(__READABLE_DF));
114    }
115    
116    /**
117     * Get the uaiCodes of the organization units involved in the groups report
118     * @param orgUnitId The parent UAI code
119     * @return if the uai code given by the user is valid, the list will contain solely this one
120     *         if it is invalid, the list will contain no element and a warning message will be displayed
121     *         if it is null, the list will contain all existing uai codes
122     */
123    public List<String> getUaiCodes(String orgUnitId)
124    {
125        List<String> uaiCodes = new ArrayList<> ();
126        
127        if (StringUtils.isEmpty(orgUnitId))
128        {
129            uaiCodes = _getDirectSubOrgUnitsUAICodes(_rootOrgUnitProvider.getRootId());
130        }
131        else
132        {
133            OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
134            // Valid uai code
135            uaiCodes.add(orgUnit.getUAICode());
136        }
137        
138        return uaiCodes;
139    }
140
141    /**
142     * Get the accronym if exists or UAI code of the orgunit given.
143     * @param orgUnit The orgUnit
144     * @return The accronym if it exists, otherwise the UAI code
145     */
146    public String getAccronymOrUaiCode(OrgUnit orgUnit)
147    {
148        return Optional.of(orgUnit)
149            .map(OrgUnit::getAcronym)
150            .filter(StringUtils::isNotBlank)
151            .orElseGet(orgUnit::getUAICode);
152    }
153    
154    /**
155     * Get the accronym if exists or UAI code of the orgunit given by the UAI code.
156     * @param uaiCode The UAI code of the orgUnit
157     * @return The accronym if it exists, otherwise the UAI code
158     */
159    public String getAccronymOrUaiCode(String uaiCode)
160    {
161        return getRootOrgUnitsByUaiCode(uaiCode).stream()
162            .findFirst()
163            .map(this::getAccronymOrUaiCode)
164            .orElse(uaiCode);
165    }
166
167    /**
168     * Retrieve the direct children's uai codes
169     * @param orgUnitId the id of the parent org unit
170     * @return the direct children's uai codes of the root organization unit
171     */
172    private List<String> _getDirectSubOrgUnitsUAICodes(String orgUnitId)
173    {
174        OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
175        List<String> orgUnitsUAICodes = new ArrayList<>();
176        
177        List<String> subOrgUnitsId = orgUnit.getSubOrgUnits();
178        for (String subOrgUnitId : subOrgUnitsId)
179        {
180            OrgUnit subOrgUnit = _resolver.resolveById(subOrgUnitId);
181            String subOrgUnitUAICode = subOrgUnit.getUAICode();
182            
183            if (subOrgUnitUAICode != null)
184            {
185                orgUnitsUAICodes.add(subOrgUnitUAICode);
186            }
187        }
188        
189        return orgUnitsUAICodes;
190    }
191
192    /**
193     * Get Programs with the current catalog, language and selected orgUnit.
194     * @param orgUnit Selected orgunit
195     * @param lang Selected language
196     * @param catalog Selected catalog
197     * @return A List of Program in the catalog, language and selected orgUnit
198     */
199    public List<Program> filterProgramsFromOrgUnits(OrgUnit orgUnit, String lang, String catalog)
200    {
201        List<Program> selectedPrograms = new ArrayList<>();
202
203        List<Expression> programExpressions = new ArrayList<>();
204        programExpressions.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
205        programExpressions.add(new LanguageExpression(Operator.EQ, lang));
206        programExpressions.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
207        
208        if (orgUnit != null)
209        {
210            // Retrouver l'arborescence sous la composante
211            List<String> orgUnits = getSubOrgUnits(orgUnit);
212
213            // Chercher les programmes concernés par la composante sélectionnée et ses enfants
214            Expression[] orgUnitsExpressions = new Expression[orgUnits.size()];
215            for (int i = 0; i < orgUnits.size(); i++)
216            {
217                String orgUnitId = orgUnits.get(i);
218                orgUnitsExpressions[i] = new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId);
219            }
220            
221            programExpressions.add(new OrExpression(orgUnitsExpressions));
222        }
223        
224        String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0])));
225        AmetysObjectIterable<Program> programs = _resolver.query(programQuery);
226        AmetysObjectIterator<Program> programsIterator = programs.iterator();
227
228        while (programsIterator.hasNext())
229        {
230            selectedPrograms.add(programsIterator.next());
231        }
232        
233        return selectedPrograms;
234    }
235    
236    /**
237     * Retrieves an organization unit with its uai code.
238     * @param uaiCode The UAI code
239     * @return the root organization units corresponding to this uai code
240     */
241    public AmetysObjectIterable<OrgUnit> getRootOrgUnitsByUaiCode(String uaiCode)
242    {
243        // Find the root organization unit corresponding to this uai code
244        Expression orgUnitExpression = new AndExpression(
245                new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE),
246                new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode)
247        );
248        String orgUnitQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, orgUnitExpression);
249        AmetysObjectIterable<OrgUnit> rootOrgUnits = _resolver.query(orgUnitQuery);
250        
251        return rootOrgUnits;
252    }
253
254    /**
255     * Get the ids of the organization units beneath the organization unit with the given id
256     * @param orgUnitId the id of the parent organization unit
257     * @return the list of child organization units ids
258     */
259    public List<String> getSubOrgUnits(String orgUnitId)
260    {
261        OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
262        return orgUnit == null ? null : getSubOrgUnits(orgUnit);
263    }
264
265    /**
266     * Get the ids of all the sub org units
267     * @param orgUnit the organization unit
268     * @return the list of child organization units ids
269     */
270    public List<String> getSubOrgUnits(OrgUnit orgUnit)
271    {
272        List<String> orgUnits = new ArrayList<>();
273        orgUnits.add(orgUnit.getId());
274        
275        for (String child : orgUnit.getSubOrgUnits())
276        {
277            orgUnits.addAll(getSubOrgUnits(child));
278        }
279        
280        return orgUnits;
281    }
282    
283    /**
284     * Format the given long
285     * @param number the long
286     * @return string representation of this long
287     */
288    public String formatNumberToSax(Long number)
289    {
290        return number > 0 ? String.valueOf(number) : "";
291    } 
292
293    /**
294     * Get the programs' iterator of all programs contained in the organization unit with the given id
295     * @param orgUnitId the id of the organization unit
296     * @param lang the lang of the programs
297     * @param catalog the catalog of the programs
298     * @return the programs iterator
299     */
300    public AmetysObjectIterable<Program> getProgramsByOrgUnitId(String orgUnitId, String lang, String catalog)
301    {
302        Expression programExpression = new AndExpression(
303                new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE),
304                new LanguageExpression(Operator.EQ, lang),
305                new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog),
306                new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId)
307        );
308        String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programExpression);
309        return _resolver.query(programQuery);
310    }
311
312    /**
313     * Get the list of courses underneath the given ametys object
314     * @param programItem The program item to gather the courses from
315     * @return the map representation of the tree of ametys objects
316     */
317    public Map<ProgramItem, Object> getCoursesFromContent(ProgramItem programItem)
318    {
319        Map<ProgramItem, Object> contentTree = new LinkedHashMap<>();
320        for (ProgramItem childProgramItem : _odfHelper.getChildProgramItems(programItem))
321        {
322            Map<ProgramItem, Object> childTree = new LinkedHashMap<>();
323
324            if (childProgramItem instanceof Course)
325            {
326                contentTree.put(childProgramItem, getCoursesFromContent(childProgramItem));
327            }
328            else
329            {
330                childTree = getCoursesFromContent(childProgramItem);
331            }
332            
333            if (MapUtils.isNotEmpty(childTree))
334            {
335                contentTree.put(childProgramItem, childTree);
336            }
337        }
338
339        return contentTree.size() == 0 ? null : contentTree;
340    }
341
342    /**
343     * Get code VRSVDI
344     * @param content the content
345     * @return the codeVRSVDI if it's set, otherwise the second part of the content code
346     */
347    public String getCodeVRSVDI(ModifiableDefaultContent content)
348    {
349        String defaultCode = StringUtils.substringAfter(((ProgramItem) content).getCode(), "-");
350        return content.getValue("codeVRSVDI", false, defaultCode);
351    }
352    
353    /**
354     * Get code DIP
355     * @param content the content
356     * @return the codeDIP if it's set, otherwise the first part of the content code
357     */
358    public String getCodeDIP(ModifiableDefaultContent content)
359    {
360        String defaultCode = StringUtils.substringBefore(((ProgramItem) content).getCode(), "-");
361        return content.getValue("codeDIP", false, defaultCode);
362    }
363
364    /**
365     * Generates SAX events for a multiple enumerated attribute. The attribute must be of type content or string
366     * @param handler The handler
367     * @param content The content
368     * @param attributeName The attribute name
369     * @param tagName The name of the tag
370     * @throws SAXException if an error occurs
371     */
372    public void saxContentAttribute(TransformerHandler handler, ModifiableDefaultContent content, String attributeName, String tagName) throws SAXException
373    {
374        Locale lang = new Locale(content.getLanguage());
375        
376        if (!ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(content.getType(attributeName).getId()))
377        {
378            throw new IllegalArgumentException("The attribute '" + attributeName + "' should be of type '" + ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID + "'.");
379        }
380        
381        Object value = content.getValue(attributeName);
382        if (value != null)
383        {
384            Stream<ContentValue> values = content.isMultiple(attributeName)
385                    ? Stream.of((ContentValue[]) value)
386                    : Stream.of((ContentValue) value);
387            
388            String tagValue =
389                    values.map(ContentValue::getContentIfExists)
390                        .filter(Optional::isPresent)
391                        .map(Optional::get)
392                        .map(c -> c.getTitle(lang))
393                        .filter(StringUtils::isNotBlank)
394                        .collect(Collectors.joining(", "));
395            
396            XMLUtils.createElement(handler, tagName, tagValue);
397        }
398    }
399    
400    /**
401     * Convert a duration in minutes to a string representing the duration in hours.
402     * @param duree in minutes
403     * @return the duration in hours
404     */
405    public String minute2hour(int duree)
406    {
407        int h = duree / 60;
408        int m = duree % 60;
409        return String.format("%dh%02d", h, m);
410    }
411    
412    /**
413     * Sax the "natures d'enseignement" from the reference table.
414     * @param handler The transformer handler
415     * @param logger The logger
416     * @throws SAXException if an error occurs
417     */
418    public void saxNaturesEnseignement(TransformerHandler handler, Logger logger) throws SAXException
419    {
420        String lang = Config.getInstance().getValue("odf.programs.lang");
421        
422        Map<String, List<OdfReferenceTableEntry>> itemsByCategory = 
423            _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE)
424                .stream()
425                .collect(Collectors.groupingBy(item -> ContentDataHelper.getContentIdFromContentData(item.getContent(), "category")));
426        
427        XMLUtils.startElement(handler, "natureEnseignement");
428        for (String categoryId : itemsByCategory.keySet())
429        {
430            Content category = null;
431            try
432            {
433                category = Optional.ofNullable(categoryId)
434                    .filter(StringUtils::isNotBlank)
435                    .map(_resolver::<Content>resolveById)
436                    .orElse(null);
437            }
438            catch (UnknownAmetysObjectException e)
439            {
440                if (StringUtils.isNotEmpty(categoryId))
441                {
442                    logger.warn("There is no content matching with the ID {}.", categoryId);
443                }
444            }
445            
446            AttributesImpl attr = new AttributesImpl();
447            attr.addCDATAAttribute("code", category == null ? StringUtils.EMPTY : category.getValue("code", false, StringUtils.EMPTY));
448            attr.addCDATAAttribute("order", category == null ? String.valueOf(Long.MAX_VALUE) : String.valueOf(category.getValue("order", false, Long.MAX_VALUE)));
449            XMLUtils.startElement(handler, "category", attr);
450            for (OdfReferenceTableEntry item : itemsByCategory.get(categoryId))
451            {
452                attr = new AttributesImpl();
453                attr.addCDATAAttribute("id", item.getId());
454                attr.addCDATAAttribute("code", item.getCode());
455                attr.addCDATAAttribute("order", String.valueOf(item.getOrder()));
456                XMLUtils.createElement(handler, "item", attr, item.getLabel(lang));
457            }
458            XMLUtils.endElement(handler, "category");
459        }
460        XMLUtils.endElement(handler, "natureEnseignement");
461    }
462    
463    /**
464     * Get the steps which can hold this program item.
465     * @param programItem The program item
466     * @return The list of steps linked to the programItem
467     */
468    public Set<Container> getSteps(ProgramItem programItem)
469    {
470        Set<Container> containers = new HashSet<>();
471        
472        // Search if the current element is a container and is of type year
473        if (programItem instanceof Container)
474        {
475            Container container = (Container) programItem;
476            if (_refTableHelper.getItemCode(container.getNature()).equals("annee"))
477            {
478                containers.add(container);
479            }
480        }
481        
482        // In all other cases, search in the parent elements
483        if (containers.isEmpty())
484        {
485            for (ProgramItem child : _odfHelper.getParentProgramItems(programItem))
486            {
487                containers.addAll(getSteps(child));
488            }
489        }
490        
491        return containers;
492    }
493    
494    /**
495     * Get the potential steps holder (step or field "etapePorteuse" in courses) of the {@link ProgramItem}.
496     * @param programItem The program item
497     * @param logger The logger
498     * @param logPrefix The log prefix
499     * @return The list of potential steps holder linked to the programItem. It there are several, there is no defined step holder.
500     */
501    public Set<Container> getStepsHolders(ProgramItem programItem, Logger logger, String logPrefix)
502    {
503        Set<Container> containers = new HashSet<>();
504        
505        // Search if the current element is a course and has a step holder
506        if (programItem instanceof Course)
507        {
508            Course course = (Course) programItem;
509            ContentValue etapePorteuse = course.getValue("etapePorteuse");
510            if (etapePorteuse != null)
511            {
512                logger.info("[{}] L'ELP {} ({}) contient une étape porteuse.", logPrefix, course.getTitle(), course.getId());
513                try
514                {
515                    containers.add((Container) etapePorteuse.getContent());
516                }
517                catch (AmetysRepositoryException e)
518                {
519                    logger.warn("[{}] L'étape porteuse {} référencée par l'ELP {} ({}) n'a pas été trouvée. Vérifiez qu'elle n'a pas été supprimée.", logPrefix, etapePorteuse.getContentId(), course.getTitle(), course.getId());
520                }
521            }
522        }
523        // Search if the current element is a container and is of type year
524        else if (programItem instanceof Container)
525        {
526            Container container = (Container) programItem;
527            if (_refTableHelper.getItemCode(container.getNature()).equals("annee"))
528            {
529                containers.add(container);
530            }
531        }
532        
533        // In all other cases, search in the parent elements
534        if (containers.isEmpty())
535        {
536            for (ProgramItem child : _odfHelper.getParentProgramItems(programItem))
537            {
538                containers.addAll(getStepsHolders(child, logger, logPrefix));
539            }
540        }
541        
542        return containers;
543    }
544}