001/*
002 *  Copyright 2020 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 */
016
017package org.ametys.plugins.odfpilotage.helper;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Objects;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.components.ContextHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.environment.Session;
038
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.repository.ModifiableDefaultContent;
041import org.ametys.cms.workflow.ContentWorkflowHelper;
042import org.ametys.core.ui.Callable;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.odf.course.Course;
045import org.ametys.odf.coursepart.CoursePart;
046import org.ametys.odf.enumeration.OdfReferenceTableHelper;
047import org.ametys.odf.orgunit.OrgUnit;
048import org.ametys.odf.program.Container;
049import org.ametys.odf.program.Program;
050import org.ametys.odf.program.SubProgram;
051import org.ametys.odf.tree.ODFContentsTreeHelper;
052import org.ametys.plugins.contentstree.TreeConfiguration;
053import org.ametys.plugins.odfpilotage.cost.CostComputationComponent;
054import org.ametys.plugins.odfpilotage.cost.entity.CostComputationData;
055import org.ametys.plugins.odfpilotage.cost.entity.CoursePartCostData;
056import org.ametys.plugins.odfpilotage.cost.entity.Effectives;
057import org.ametys.plugins.odfpilotage.cost.entity.EqTD;
058import org.ametys.plugins.odfpilotage.cost.entity.NormDetails;
059import org.ametys.plugins.odfpilotage.cost.entity.OverriddenData;
060import org.ametys.plugins.odfpilotage.cost.entity.VolumesOfHours;
061import org.ametys.plugins.repository.AmetysRepositoryException;
062import org.ametys.plugins.repository.UnknownAmetysObjectException;
063import org.ametys.runtime.model.ModelItem;
064
065import com.opensymphony.workflow.WorkflowException;
066
067/**
068 * This component handle the content of the cost modeling tool
069 */
070public class CostComputationTreeHelper extends ODFContentsTreeHelper implements Contextualizable
071{
072    private static int _ACTION = 2;
073    
074    private static final String __COST_DATA_KEY = "cost-data-key";
075    
076    /** The cost computation component */
077    protected CostComputationComponent _costComputationComponent;
078   
079    /** Workflow helper component */
080    protected ContentWorkflowHelper _contentWorkflowHelper;
081
082    /** The ODF enumeration helper */
083    protected OdfReferenceTableHelper _refTableHelper;
084    
085    /** The current user provider */
086    protected CurrentUserProvider _currentUserProvider;
087    
088    /** The context */
089    protected Context _context;
090    
091    @Override
092    public void contextualize(Context context) throws ContextException
093    {
094        _context = context;
095    }
096    
097    @Override 
098    public void service(ServiceManager smanager) throws ServiceException
099    {
100        super.service(smanager);
101        _costComputationComponent = (CostComputationComponent) smanager.lookup(CostComputationComponent.ROLE);
102        _refTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE);
103        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
104    }
105    
106    /**
107     * Get the cost data from user session
108     * @return the cost data
109     */
110    protected CostComputationData _getCostData()
111    {
112        Request request = ContextHelper.getRequest(_context);
113        Session session = request.getSession();
114        
115        CostComputationData costData = (CostComputationData) session.getAttribute(__COST_DATA_KEY);
116        if (costData == null)
117        {
118            throw new UnsupportedOperationException("Cost data is not initalized in session attribute '" + __COST_DATA_KEY + "'");
119        }
120        
121        return costData;
122    }
123    
124    /**
125     * Set the cost data to user session
126     * @param costData the cost data
127     */
128    protected void setCostData(CostComputationData costData)
129    {
130        Request request = ContextHelper.getRequest(_context);
131        Session session = request.getSession();
132        
133        session.setAttribute(__COST_DATA_KEY, costData);
134    }
135    
136    /**
137     * Launch the cost computation component algorithm 
138     * @param content the content to compute
139     * @param catalog the catalog
140     * @param lang the lang
141     * @param overriddenData overridden data by the user
142     */
143    public void launchProgram(Content content, String catalog, String lang, OverriddenData overriddenData)
144    {
145        if (content instanceof OrgUnit)
146        {
147            setCostData(_costComputationComponent.computeCostsOnOrgUnits((OrgUnit) content, catalog, lang, overriddenData));
148        }
149        else if (content instanceof Program)
150        {
151            setCostData(_costComputationComponent.computeCostsOnProgram((Program) content, overriddenData));
152        }
153    }
154    
155    /**
156     * Get the children contents according the tree configuration
157     * @param parentContent the root content
158     * @param treeConfiguration the tree configuration
159     * @param catalog the catalog
160     * @param lang the lang
161     * @return the children content for each child attributes
162     */
163    public Map<String, List<Content>> getChildrenContent(Content parentContent, TreeConfiguration treeConfiguration, String catalog, String lang)
164    {
165        Map<String, List<Content>> children = new HashMap<>();
166        if (parentContent instanceof OrgUnit)
167        {
168            OrgUnit orgUnit = (OrgUnit) parentContent;
169            List<Content> contents = orgUnit.getSubOrgUnits()
170                    .stream()
171                    .map(this::_resolveSilently)
172                    .filter(Objects::nonNull)
173                    .collect(Collectors.toList());
174            if (orgUnit.getParentOrgUnit() == null && !contents.isEmpty())
175            {
176                children.put("childOrgUnits", contents);
177            }
178            else
179            {
180                List<Program> programs = _odfHelper.getProgramsFromOrgUnit((OrgUnit) parentContent, catalog, lang);
181                contents = programs.stream()
182                        .filter(Content.class::isInstance)
183                        .map(Content.class::cast)
184                        .collect(Collectors.toList());
185
186                // _programsLink n'est pas un vrai attribut, cet identifiant n'est jamais utilisé 
187                children.put("_programsLink", contents);
188            }
189        }
190        else
191        {
192            children = super.getChildrenContent(parentContent, treeConfiguration);
193        }
194        return children;
195    }
196    
197    /**
198     * Get the children contents according the tree configuration
199     * @param contentId the parent content
200     * @param treeId the tree configuration
201     * @param contentPath the content path
202     * @param catalog the catalog
203     * @param lang the lang
204     * @return the children content
205     */
206    @Callable
207    public Map<String, Object> getChildrenContent(String contentId, String treeId, String contentPath, String catalog, String lang)
208    {
209        TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId);
210        Content parentContent = _getParentContent(contentId);
211        Map<String, List<Content>> children = getChildrenContent(parentContent, treeConfiguration, catalog, lang);
212   
213        Map<String, Object> infos = new HashMap<>();
214
215        List<Map<String, Object>> childrenInfos = new ArrayList<>();
216        infos.put("children", childrenInfos);
217
218        for (String attributePath : children.keySet())
219        {
220            for (Content childContent : children.get(attributePath))
221            {
222                Map<String, Object> childInfo = content2Json(childContent, contentPath + ModelItem.ITEM_PATH_SEPARATOR + childContent.getName());
223                childInfo.put("metadataPath", attributePath);
224                
225                if (!hasChildrenContent(childContent, treeConfiguration))
226                {
227                    childInfo.put("children", Collections.EMPTY_LIST);
228                }
229
230                childrenInfos.add(childInfo);
231            }
232        }
233        return infos;
234    }
235    
236    /**
237     * Get the root node informations
238     * @param contentId The content
239     * @param catalog the catalog
240     * @param lang the lang
241     * @param overriddenData overridden data by the user
242     * @return The informations
243     */
244    @Callable
245    public Map<String, Object> getRootNodeInformations(String contentId, String catalog, String lang, Map<String, Map<String, String>> overriddenData)
246    {
247        return getNodeInformations(contentId, catalog, lang, overriddenData);
248    }
249    
250    /**
251     * Get the node informations
252     * @param contentId The content
253     * @param catalog the catalog
254     * @param lang the lang
255     * @param overriddenData Overridden data by the user
256     * @return The informations
257     */
258    @Callable
259    public Map<String, Object> getNodeInformations(String contentId, String catalog, String lang, Map<String, Map<String, String>> overriddenData)
260    {
261        Content content = _ametysResolver.resolveById(contentId);
262        OverriddenData data = new OverriddenData(overriddenData);
263        launchProgram(content, catalog, lang, data);
264        Map<String, Object> json = content2Json(content, content.getName());
265        if (content instanceof Program)
266        {
267            json.put("lang", content.getLanguage());
268            json.put("catalog", ((Program) content).getCatalog());
269        }
270        return json;
271    }
272    
273    /**
274     * Get the default JSON representation of a content of the tree
275     * @param content the content
276     * @param contentPath the content path
277     * @return the content as JSON
278     */
279    protected Map<String, Object> content2Json(Content content, String contentPath)
280    {
281        Map<String, Object> infos = super.content2Json(content);
282       
283        CostComputationData costData = _getCostData();
284        
285        // If the content is a program then we can launch the algorithm
286        if (content instanceof OrgUnit)
287        {
288            _handleOrgUnit(content, infos, contentPath, costData);
289        }
290        else if (content instanceof Program)
291        {
292            _handleProgram(content, infos, contentPath, costData);
293        }
294        else if (content instanceof SubProgram)
295        {
296            _handleSubProgram(content, infos, contentPath, costData);
297        }
298        else if (content instanceof Container)
299        {
300            _handleContainer(content, infos, contentPath, costData);
301        }
302        else if (content instanceof Course)
303        {
304            _handleCourse(content, infos, contentPath, costData);
305        }
306        else if (content instanceof CoursePart)
307        {
308            _handleCoursePart(content, infos, contentPath, costData);
309        }
310        
311        if (costData.getEffectiveByStep(content.getId()) != null)
312        {
313            Map<String, Double> parents = costData.getEffectiveByStep(content.getId()).entrySet()
314                    .stream()
315                    .collect(
316                        Collectors.toMap(
317                            e -> _getTitle(e.getKey().getId()),
318                            Map.Entry::getValue,
319                            (title1, title2) -> title1
320                        )
321                    );
322
323            infos.put("parents", parents);
324        }
325        return infos;
326    }
327    
328    private String _getTitle(String id)
329    {
330        return ((Container) _ametysResolver.resolveById(id)).getTitle();
331    }
332  
333    /**
334     * Retrieve hourly volume of a content and add it to the JSON structure
335     * @param json the structure
336     * @param content the content
337     * @param costData the cost data
338     */
339    protected void _addVolumeHoraire(Map<String, Object> json, Content content, CostComputationData costData)
340    {
341        VolumesOfHours volumeHoraire = costData.getVolumesOfHours(content.getId());
342
343        for (String natureId : volumeHoraire.getVolumes().keySet())
344        {
345            Map<String, Object> data = new HashMap<>();
346            if (content instanceof CoursePart)
347            {
348                data.put("volHorCoursePart", volumeHoraire.getVolumes().get(natureId));
349            }
350            else
351            {
352                data.put("volHorProgramItem", volumeHoraire.getVolumes().get(natureId));
353            }
354            json.put(natureId, data);
355        }
356
357    }
358    
359    /**
360     * Retrieve the number of groups and add it to the JSON structure
361     * @param json the JSON structure
362     * @param groupeAOuvrir groups to open
363     * @param groupeCalcule calculated groups
364     */
365    protected void _addGroupe(Map<String, Object> json, Long groupeAOuvrir, Long groupeCalcule)
366    {
367        Map<String, Object> data = new HashMap<>();
368        if (groupeAOuvrir > 0)
369        {
370            data.put("groupeAOuvrir", groupeAOuvrir);
371            json.put("groupeCalcule", groupeCalcule);
372        }
373        else
374        {
375            data.put("groupeCalcule", groupeCalcule);
376        }
377        json.put("groupe", data);
378    }
379    
380    /**
381     * Retrieve the effective and add it to the JSON structure
382     * @param json the JSON structure
383     * @param content the content
384     * @param contentPath the content path
385     * @param costData the cost data
386     */
387    protected void _addEffectif(Map<String, Object> json, Content content, String contentPath, CostComputationData costData)
388    {
389        Map<String, Object> data = new HashMap<>();
390        Effectives effectives = costData.getEffective(content.getId());
391
392        if (effectives.getEnteredEffective().isPresent())
393        {
394            data.put("effectifPrev", Math.round(effectives.getEnteredEffective().get()));
395        }
396        else
397        {
398            data.put("effectifCalc", Math.round(effectives.getComputedGlobalEffective()));
399        }
400
401        json.put("effectifGlobal", data);
402        json.put("effectifLocal", Math.round(effectives.getComputedLocalEffective(contentPath)));
403
404    }
405    
406    /**
407     * Retrieve the TD equivalent value and add it to the JSON structure
408     * @param json the JSON structure
409     * @param content the content
410     * @param contentPath the content path
411     * @param costData the cost data
412     */
413    protected void _addEqTD(Map<String, Object> json, Content content, String contentPath, CostComputationData costData)
414    {
415        EqTD eqTD = costData.getEqTD(content.getId());
416
417        json.put("eqtdLocal", eqTD.getLocalEqTD(contentPath));
418        json.put("eqtdProratise", eqTD.getProRatedEqTD(contentPath));
419        if (eqTD.getGlobalEqTD() != 0d)
420        {
421            json.put("eqtdGlobal", eqTD.getGlobalEqTD());
422
423        }
424    }
425
426    /**
427     * Compute the H/E report and add it to the JSON structure
428     * @param json the JSON structure
429     * @param content the content
430     * @param contentPath the content path
431     * @param costData the cost data
432     */
433    protected void _addRapportHE(Map<String, Object> json, Content content, String contentPath, CostComputationData costData)
434    {
435        json.put("rapp", costData.getHeReport(contentPath));
436    }
437    
438    
439    /**
440     * Handle coursePart informations
441     * @param content the content
442     * @param json the JSON structure
443     * @param contentPath the content path
444     * @param costData the cost data
445     */
446    protected void _handleCoursePart(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
447    {
448        CoursePartCostData coursePartCostData = costData.getCoursePartCostData().get(content);
449        
450        if (coursePartCostData != null)
451        {
452            _addEffectif(json, content, contentPath, costData);
453            _addGroupe(json, coursePartCostData.getGroupsToOpen(), coursePartCostData.getCalculatedGroups());
454            _handleODFElement(content, json, contentPath, costData);
455            _addNorm(json, coursePartCostData.getNormDetails());
456        }
457    }
458    
459    /**
460     * Add norm informations
461     * @param json the JSON structure
462     * @param normDetails the norm details
463     */
464    protected void _addNorm(Map<String, Object> json, NormDetails normDetails)
465    {
466        Map<String, Object> data = new HashMap<>();
467        data.put("effMax", normDetails.getEffectiveMax());
468        data.put("effMin", normDetails.getEffectiveMinSup());
469        json.put("norme", data);
470    }
471
472    /**
473     * Handle sub program informations
474     * @param content the content
475     * @param json the JSON structure
476     * @param contentPath the current path
477     * @param costData the cost data
478     */
479    protected void _handleSubProgram(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
480    {
481        _addEffectif(json, content, contentPath, costData);
482        _handleODFElement(content, json, contentPath, costData);
483    }
484    
485    /**
486     * Handle program informations
487     * @param content the content
488     * @param json the JSON structure
489     * @param contentPath the current path
490     * @param costData the cost data
491     */
492    protected void _handleProgram(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
493    {
494        _addEffectif(json, content, contentPath, costData);
495        _handleODFElement(content, json, contentPath, costData);
496    }
497    
498    /**
499     * Handle orgUnit informations
500     * @param content the content
501     * @param json the JSON structure
502     * @param contentPath the current path
503     * @param costData the cost data
504     */
505    protected void _handleOrgUnit(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
506    {
507        _addEffectif(json, content, contentPath, costData);
508        _handleODFElement(content, json, contentPath, costData);
509    }
510    
511    private void _handleCourse(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
512    {
513        _addEffectif(json, content, contentPath, costData);
514        _handleODFElement(content, json, contentPath, costData);
515    }
516
517    /**
518     * Fill data in JSON describing the year container
519     * @param content the year container
520     * @param json the structure
521     * @param contentPath the content path
522     * @param costData the cost data
523     */
524    protected void _handleContainer(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
525    {
526        Map<String, Object> data = new HashMap<>();
527        Optional<Double> effectif = costData.getGlobalComputedEffective(content.getId());
528        
529        if (effectif.isPresent())
530        {
531            if (_refTableHelper.getItemCode(((Container) content).getNature()).equals("annee"))
532            {
533                data.put("effectifYear", effectif.get());
534                json.put("effectifGlobal", data);
535                json.put("effectifLocal", data);
536            }
537            else
538            {
539                _addEffectif(json, content, contentPath, costData);
540            }
541            _handleODFElement(content, json, contentPath, costData);
542        }
543    }
544    
545    /**
546     * Fill data in JSON describing an ODF Element
547     * @param content the year container
548     * @param json the structure
549     * @param contentPath the content path
550     * @param costData the cost data
551     */
552    protected void _handleODFElement(Content content, Map<String, Object> json, String contentPath, CostComputationData costData)
553    {
554        _addVolumeHoraire(json, content, costData);
555        _addEqTD(json, content, contentPath, costData);
556        _addRapportHE(json, content, contentPath, costData);
557    }
558    
559    private Content _resolveSilently(String contentId)
560    {
561        try 
562        {
563            return _ametysResolver.resolveById(contentId);
564        }
565        catch (UnknownAmetysObjectException e) 
566        {
567            return null;
568        }
569    }
570    
571    /**
572     * Launch the cost computation component algorithm with overridden data by the user 
573     * @param contentsToRefresh all open contents in the tool to refresh
574     * @param contentId the root node
575     * @param catalog the catalog
576     * @param lang the lang
577     * @param overridenData overridden data by the user
578     * @return new values associated with their path
579     */
580    @Callable 
581    public Map<String, Map<String, Object>> refresh(Map<String, String> contentsToRefresh, String contentId, String catalog, String lang, Map<String, Map<String, String>> overridenData)
582    {
583        Map<String, Map<String, Object>> refreshedData = new HashMap<>();
584        Content content = _ametysResolver.resolveById(contentId);
585        OverriddenData data = new OverriddenData(overridenData);
586        
587        launchProgram(content, catalog, lang, data);
588        for (Entry<String, String> entry : contentsToRefresh.entrySet())
589        {
590            Content item = _ametysResolver.resolveById(entry.getValue());
591            refreshedData.put(entry.getKey(), content2Json(item, entry.getKey()));
592        }
593        return refreshedData;
594    }
595    
596    /**
597     * Retrieve old data to compare with the new computed one
598     * @return all old data to compare
599     */
600    @Callable
601    public Map<String, Map<String, Double>> getOldData()
602    {
603        CostComputationData costData = _getCostData();
604
605        Map<String, Map<String, Double>> data = new HashMap<>();
606        data.put("heReport", costData.getHeReport());
607        data.put("eqtdPorte", costData.getEqTDPorte());
608        data.put("eqTDproratise", costData.getEqTDProratise());
609        data.put("eqTDglobal", costData.getEqTDGlobal());
610        
611        return data;
612    }
613    
614    /**
615     * Save overridden data to the contents
616     * @param overriddenData overridden data by the user
617     * @return true if the saving is successful
618     * @throws WorkflowException if an error occurred
619     * @throws AmetysRepositoryException if an error occurred
620     */
621    @Callable
622    public Boolean saveOverriddenData(Map<String, Map<String, String>> overriddenData) throws AmetysRepositoryException, WorkflowException
623    {
624        Boolean hasChanged = false;
625        for (Entry<String, Map<String, String>> data : overriddenData.entrySet())
626        {
627            ModifiableDefaultContent content = _ametysResolver.resolveById(data.getKey());
628            if (data.getValue().containsKey("effectifLocal") && content instanceof Container)
629            {
630                content.setValue("numberOfStudentsEstimated", Long.parseLong(data.getValue().get("effectifLocal")));
631                hasChanged = true;
632            }
633            if (data.getValue().containsKey("groupe") && content instanceof CoursePart)
634            {
635                content.setValue("groupsToOpen", Long.parseLong(data.getValue().get("groupe")));
636                hasChanged = true;
637            }
638            if (content instanceof CoursePart && _containsTeachingNature(data.getValue().keySet()))
639            {
640                for (String nbHours: data.getValue().values())
641                {
642                    content.setValue("nbHours", Double.parseDouble(nbHours));
643                    hasChanged = true;
644                }
645            }
646            if (hasChanged)
647            {
648                _contentWorkflowHelper.doAction(content, _ACTION);
649            }
650        }
651        return hasChanged;
652    }
653    
654    private Boolean _containsTeachingNature(Set<String> overriddenType)
655    {
656        List<String> teachingNature = _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE).stream()
657                .map(t -> t.getCode())
658                .collect(Collectors.toList());
659        
660        return overriddenType.stream()
661                .anyMatch(o -> teachingNature.contains(o));
662    }
663}