001/*
002 *  Copyright 2010 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.course;
017
018import java.time.LocalDate;
019import java.util.Arrays;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.function.Predicate;
028import java.util.stream.Collectors;
029
030import javax.jcr.Node;
031
032import org.apache.commons.lang3.ArrayUtils;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.data.ContentDataHelper;
036import org.ametys.cms.data.ContentValue;
037import org.ametys.cms.data.RichText;
038import org.ametys.cms.repository.ModifiableDefaultContent;
039import org.ametys.odf.ProgramItem;
040import org.ametys.odf.courselist.CourseList;
041import org.ametys.odf.courselist.CourseListContainer;
042import org.ametys.odf.coursepart.CoursePart;
043import org.ametys.odf.data.EducationalPath;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
046import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
047import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater;
048import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry;
049import org.ametys.runtime.model.exception.UndefinedItemPathException;
050
051/**
052 * Class representing a {@link Course}
053 */
054public class Course extends ModifiableDefaultContent<CourseFactory> implements CourseListContainer, ProgramItem
055{
056    /** Name of attribute for parent course lists */
057    public static final String PARENT_COURSE_LISTS = "parentCourseLists";
058    
059    /** Name of attribute for parent course lists */
060    public static final String CHILD_COURSE_LISTS = "courseLists";
061    
062    /** Mandatory Identifier to generate the CDM-fr id */
063    public static final String CDM_CODE = "cdmCode";
064    
065    /** Constants for ects attribute */
066    public static final String ECTS = "ects";
067    
068    /** Constants for path of ects attribute by education path */
069    public static final String ECTS_BY_PATH = "dataByEducationalPath/ects";
070
071    /** Constants for level attribute */
072    public static final String LEVEL = "level";
073
074    /** Constants for description attribute */
075    public static final String DESCRIPTION = "description";
076
077    /** Constants for objectives attribute */
078    public static final String OBJECTIVES = "objectives";
079
080    /** Constants for nbHours attribute */
081    public static final String NUMBER_OF_HOURS = "nbHours";
082
083    /** Constants for neededPrerequisite attribute */
084    public static final String NEEDED_PREREQUISITE = "neededPrerequisite";
085    
086    /** Constants for teds attribute */
087    public static final String TEDS = "teds";
088
089    /** Constants for formOfAssessment attribute */
090    public static final String FORM_OF_ASSESSMENT = "formOfAssessment";
091    
092    /** Constants for attribute 'requiredSkills' */
093    public static final String REQUIRED_SKILLS = "requiredSkills";
094    
095    /** Constants for repeater 'acquiredSkills' */
096    public static final String ACQUIRED_SKILLS = "acquiredSkills";
097
098    /** The attribute name of the skills */
099    public static final String ACQUIRED_SKILLS_SKILLSET = "skillSet";
100    
101    /** The attribute name of the skills in acquiredSkills */
102    public static final String ACQUIRED_SKILLS_SKILLS = "skills";
103
104    /** The skill attribute name in acquiredSkills/skills */
105    public static final String ACQUIRED_SKILLS_SKILLS_SKILL = "skill";
106    
107    /** The skill acquisition level attribute name in acquiredSkills/skills */
108    public static final String ACQUIRED_SKILLS_SKILLS_ACQUISITION_LEVEL = "acquisitionLevel";
109
110    /** Constants for syllabus attribute */
111    public static final String SYLLABUS = "syllabus";
112
113    /** Constants for additionalInformations attribute */
114    public static final String ADDITIONAL_INFORMATIONS = "additionalInformations";
115
116    /** Constants for erasmusCode attribute */
117    public static final String ERASMUS_CODE = "erasmusCode";
118
119    /** Constants for teachingLocation attribute */
120    public static final String TEACHING_LOCATION = "teachingLocation";
121
122    /** Constants for maxNumberOfStudents attribute */
123    public static final String MAX_NUMBER_OF_STUDENTS = "maxNumberOfStudents";
124
125    /** Constants for teachingTerm attribute */
126    public static final String TEACHING_TERM = "teachingTerm";
127
128    /** Constants for timeSlot attribute */
129    public static final String TIME_SLOT = "timeSlot";
130
131    /** Constants for trainingCourseDuration attribute */
132    public static final String TRAINING_COURSE_DURATION = "trainingCourseDuration";
133
134    /** Constants for teachingMethod attribute */
135    public static final String FORMODFTEACHING_METHOD = "formofteachingMethod";
136    
137    /** Constants for formofteachingOrg attribute */
138    public static final String FORMOFTEACHING_ORG = "formofteachingOrg";
139
140    /** Constants for formOfTeaching attribute */
141    public static final String TEACHING_ACTIVITY = "teachingActivity";
142
143    /** Constants for teachingLanguage attribute */
144    public static final String TEACHING_LANGUAGE = "teachingLanguage";
145
146    /** Constants for startDate attribute */
147    public static final String START_DATE = "startDate";
148
149    /** Constants for keywords attribute */
150    public static final String KEYWORDS = "keywords";
151
152    /** Constants for webLinkLabel attribute */
153    public static final String WEB_LINK_LABEL = "webLinkLabel";
154
155    /** Constants for webLinkUrl attribute */
156    public static final String WEB_LINK_URL = "webLinkUrl";
157
158    /** Constants for lomSheets attribute */
159    public static final String LOM_SHEETS = "lomSheets";
160
161    /** Constants for LOM Sheet URL attribute */
162    public static final String LOM_SHEET_URL = "linkUrl";
163    
164    /** Constants for LOM Sheet label attribute */
165    public static final String LOM_SHEET_LABEL = "linkLabel";
166    
167    /** Constants for attribute 'contacts' */
168    public static final String CONTACTS = "contacts";
169    
170    /** Constants for attribute 'contacts/role' */
171    public static final String CONTACTS_ROLE = "role";
172    
173    /** Constants for attribute 'contacts/persons' */
174    public static final String CONTACTS_PERSONS = "persons";
175    
176    /** Constants for attribute 'courseType' */
177    public static final String COURSE_TYPE = "courseType";
178    
179    /** Constants for attribute 'bibliography' */
180    public static final String BIBLIOGRAPHY = "bibliography";
181
182    /** Constants for attribute 'skills' */
183    public static final String SKILLS = "skills";
184
185    /** Constants for attribute 'openToExchangeStudents' */
186    public static final String OPEN_TO_EXCHANGE_STUDENTS = "openToExchangeStudents";
187    
188    /** Constants for attribute 'courseParts' */
189    public static final String CHILD_COURSE_PARTS = "courseParts";
190
191    private String _contextPath;
192
193    private List<EducationalPath> _currentEducationalPath;
194
195    /**
196     * Constructor.
197     * @param node the JCR Node.
198     * @param parentPath the parent path
199     * @param factory the corresponding factory.
200     */
201    public Course(Node node, String parentPath, CourseFactory factory)
202    {
203        super(node, parentPath, factory);
204    }
205
206    @Override
207    public List<CourseList> getCourseLists()
208    {
209        return Arrays.stream(getValue(CHILD_COURSE_LISTS, false, new ContentValue[0]))
210                .map(ContentValue::getContentIfExists)
211                .flatMap(Optional::stream)
212                .map(CourseList.class::cast)
213                .collect(Collectors.toList());
214    }
215    
216    /**
217     * Get the parent course lists
218     * @return The parent course lists
219     */
220    public List<CourseList> getParentCourseLists()
221    {
222        return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0]))
223                .map(ContentValue::getContentIfExists)
224                .flatMap(Optional::stream)
225                .map(CourseList.class::cast)
226                .collect(Collectors.toList());
227    }
228
229    @Override
230    public boolean hasCourseLists()
231    {
232        return !ContentDataHelper.isMultipleContentDataEmpty(this, CHILD_COURSE_LISTS);
233    }
234    
235    @Override
236    public boolean containsCourseList(String clId)
237    {
238        return ArrayUtils.contains(ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, CHILD_COURSE_LISTS), clId);
239    }
240    
241    // --------------------------------------------------------------------------------------//
242    // CONTACTS
243    // --------------------------------------------------------------------------------------//
244
245    /**
246     * Return the list of Persons in charge binded to this program
247     * @return the id of contacts
248     */
249    public Set<String> getContacts()
250    {
251        Set<String> contactIds = new HashSet<>();
252        
253        ModelAwareRepeater contacts = getRepeater(CONTACTS);
254        if (contacts != null)
255        {
256            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
257            {
258                // Remove empty values
259                contactIds.addAll(ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
260                        .filter(contentId -> !contentId.isEmpty())
261                        .collect(Collectors.toSet()));
262            }
263        }
264
265        return Collections.EMPTY_SET;
266    }
267    
268    /**
269     * Return the list of Persons in charge binded to this program
270     * @return a list of roles and UUID
271     */
272    public Map<String, List<String>> getContactsByRole()
273    {
274        Map<String, List<String>> contactsByRole = new HashMap<>();
275        
276        ModelAwareRepeater contacts = getRepeater(CONTACTS);
277        if (contacts != null)
278        {
279            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
280            {
281                // Remove empty values
282                List<String> persons = ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
283                                                    .filter(StringUtils::isNotEmpty)
284                                                    .collect(Collectors.toList());
285                if (!persons.isEmpty())
286                {
287                    String role = ContentDataHelper.getContentIdFromContentData(contactEntry, CONTACTS_ROLE);
288                    contactsByRole.put(role, persons);
289                }
290            }
291        }
292
293        return contactsByRole;
294    }
295   
296    
297    // --------------------------------------------------------------------------------------//
298    // CONTEXT
299    // --------------------------------------------------------------------------------------//
300    
301    // Méthodes utilisées lors du parcours d'une maquette uniquement, afin de contextualiser l'élément
302    // A ne pas utiliser n'importe ou ni n'importe comment
303    
304    /**
305     * Set the parent path for links and breadcrumb
306     * @param path the parent path
307     */
308    public void setContextPath (String path)
309    {
310        _contextPath = path;
311    }
312    
313    /**
314     * Get the parent path. Can be null.
315     * @return the parent path
316     */
317    public String getContextPath ()
318    {
319        return _contextPath;
320    }
321    
322    /**
323     * Set the current educational paths of this course
324     * @param paths the current educational paths
325     */
326    public void setCurrentEducationalPaths(List<EducationalPath> paths)
327    {
328        _currentEducationalPath = paths;
329    }
330    
331    /**
332     * Get the current educational paths of this course
333     * @return the current educational path
334     */
335    public List<EducationalPath> getCurrentEducationalPaths()
336    {
337        return _currentEducationalPath;
338    }
339    
340    
341    // --------------------------------------------------------------------------------------//
342    // GETTERS AND SETTERS
343    // --------------------------------------------------------------------------------------//
344    @Override
345    public String getCatalog()
346    {
347        return getValue(CATALOG);
348    }
349    
350    @Override
351    public void setCatalog(String catalog) throws AmetysRepositoryException
352    {
353        setValue(CATALOG, catalog);
354    }
355    
356    @Override
357    public String getCode()
358    {
359        return getValue(CODE, false, StringUtils.EMPTY);
360    }
361    
362    @Override
363    public void setCode(String code) throws AmetysRepositoryException
364    {
365        setValue(CODE, code);
366    }
367    
368    public boolean isPublishable()
369    {
370        return getInternalDataHolder().getValue(PUBLISHABLE, true);
371    }
372    
373    public void setPublishable(boolean isPublishable)
374    {
375        getInternalDataHolder().setValue(PUBLISHABLE, isPublishable);
376    }
377    
378    public List<String> getOrgUnits()
379    {
380        try
381        {
382            return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS_REFERENCES);
383        }
384        catch (UndefinedItemPathException e)
385        {
386            return Collections.EMPTY_LIST; // this attribute is not part of model
387        }
388    }
389    
390    /**
391     * Get the description
392     * @return the description or null
393     */
394    public RichText getDescription()
395    {
396        return getValue(DESCRIPTION);
397    }
398
399    /**
400     * Get the objectives
401     * @return objectives
402     */
403    public RichText getObjectives()
404    {
405        return getValue(OBJECTIVES);
406    }
407    
408    /**
409     * Get the course parts.
410     * @return The {@link List} of attached {@link CoursePart}s
411     */
412    public List<CoursePart> getCourseParts()
413    {
414        return Arrays.stream(getValue(CHILD_COURSE_PARTS, false, new ContentValue[0]))
415                .map(ContentValue::getContentIfExists)
416                .flatMap(Optional::stream)
417                .map(CoursePart.class::cast)
418                .collect(Collectors.toList());
419    }
420    
421    /**
422     * Get the needed prerequisites
423     * @return the needed prerequisites or null if not set
424     */
425    public RichText getNeededPrerequisite()
426    {
427        return getValue(NEEDED_PREREQUISITE);
428    }
429
430    /**
431     * Get the formOfAssessment attribute
432     * @return the formOfAssessment or null if not set
433     */
434    public RichText getFormOfAssessment()
435    {
436        return getValue(FORM_OF_ASSESSMENT);
437    }
438    
439    /**
440     * Get the syllabus
441     * @return the syllabus or null if not set
442     */
443    public RichText getSyllabus()
444    {
445        return getValue(SYLLABUS);
446    }
447    
448    /**
449     * Get the list of LOM sheets
450     * @return the list of LOMsheets or an empty list
451     */
452    public Set<LOMSheet> getLOMSheets()
453    {
454        Set<LOMSheet> lomSheets = new HashSet<>();
455        
456        ModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS);
457        if (attributeLOMSheets != null)
458        {
459            for (ModelAwareRepeaterEntry attributeLOMSheet : attributeLOMSheets.getEntries())
460            {
461                LOMSheet lomSheet = new LOMSheet(attributeLOMSheet.getValue(LOM_SHEET_URL, false, StringUtils.EMPTY), attributeLOMSheet.getValue(LOM_SHEET_LABEL, false, StringUtils.EMPTY));
462                lomSheets.add(lomSheet);
463            }
464        }
465
466        return lomSheets;
467    }
468    
469    /**
470     * Set the LOM sheets
471     * @param sheets The LOM sheets
472     */
473    public void setLOMSheets (Set<LOMSheet> sheets)
474    {
475        // Remove old LOM sheets if exist
476        removeValue(LOM_SHEETS);
477        
478        ModifiableModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS, true);
479        for (LOMSheet lomSheet : sheets)
480        {
481            ModifiableModelAwareRepeaterEntry attributeLOMSheet = attributeLOMSheets.addEntry();
482            attributeLOMSheet.setValue(LOM_SHEET_URL, lomSheet.getUrl());
483            attributeLOMSheet.setValue(LOM_SHEET_LABEL, lomSheet.getLabel());
484        }
485    }
486    
487    /**
488     * Determines if the course has LOM sheet
489     * @param lomSheet The LOM sheet to test
490     * @return <code>true</code> if the course has LOM sheet
491     */
492    public boolean hasLOMSheet (LOMSheet lomSheet)
493    {
494        for (LOMSheet lom : getLOMSheets())
495        {
496            if (lom.getUrl().equals(lomSheet.getUrl()) && lom.getLabel().equals(lomSheet.getLabel()))
497            {
498                return true;
499            }
500        }
501        
502        return false;
503    }
504    
505    /**
506     * Get the additional information
507     * @return the additional information
508     */
509    public RichText getAdditionalInformations()
510    {
511        return getValue(ADDITIONAL_INFORMATIONS);
512    }
513
514    /**
515     * Get the Erasmus code
516     * @return the Erasmus code
517     */
518    public String getErasmusCode()
519    {
520        return ContentDataHelper.getContentIdFromContentData(this, ERASMUS_CODE);
521    }
522
523    /**
524     * Get the teaching location
525     * @return the teaching location
526     */
527    public String[] getTeachingLocation()
528    {
529        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LOCATION);
530    }
531
532    /**
533     * Set the teaching location
534     * @param teachingLocation the teaching location to set
535     * @throws AmetysRepositoryException if failed to set attribute
536     */
537    public void setTeachingLocation(String[] teachingLocation) throws AmetysRepositoryException
538    {
539        setValue(Course.TEACHING_LOCATION, teachingLocation);
540    }
541
542    /**
543     * Get the ECTS
544     * @return the ECTS
545     */
546    public double getEcts()
547    {
548        return getValue(ECTS, false, 0D);
549    }
550    
551    
552    /**
553     * Get the ECTS for a given educational path
554     * @param ctxEducationalPath The educational path
555     * @return the ECTS for this educational path
556     */
557    public double getEcts(EducationalPath ctxEducationalPath)
558    {
559        return getEcts(
560            Optional.ofNullable(ctxEducationalPath)
561                .map(List::of)
562                .orElseGet(List::of)
563        );
564    }
565    
566    /**
567     * Get the ECTS for given educational paths
568     * @param ctxEducationalPaths The educational paths
569     * @return the ECTS for this educational path
570     */
571    public double getEcts(List<EducationalPath> ctxEducationalPaths)
572    {
573        return Optional.ofNullable(ctxEducationalPaths)
574            .filter(Predicate.not(List::isEmpty))
575            .flatMap(paths -> _getFactory()._getODFHelper().<Double>getValueForPath(this, ECTS_BY_PATH, paths))
576            .orElseGet(this::getEcts);
577    }
578    
579    /**
580     * Get the number of hours
581     * @return the number of hours
582     */
583    public double getNumberOfHours()
584    {
585        return getValue(NUMBER_OF_HOURS, false, 0D);
586    }
587
588    /**
589     * Get the effectives
590     * @return the effectives
591     * @throws AmetysRepositoryException if failed to get attribute
592     */
593    public String getMaxNumberOfStudents()
594    {
595        return getValue(MAX_NUMBER_OF_STUDENTS, false, StringUtils.EMPTY);
596    }
597
598    /**
599     * Get the teaching term
600     * @return the teaching term
601     */
602    public String getTeachingTerm()
603    {
604        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_TERM);
605    }
606
607    /**
608     * Get the level
609     * @return the level
610     */
611    public String getLevel()
612    {
613        return ContentDataHelper.getContentIdFromContentData(this, LEVEL);
614    }
615
616    /**
617     * Get the teaching method
618     * @return the teaching method
619     */
620    public String getFormOfTeachingMethod()
621    {
622        return ContentDataHelper.getContentIdFromContentData(this, FORMODFTEACHING_METHOD);
623    }
624
625    /**
626     * Get the teaching organizations
627     * @return the teaching organizations
628     */
629    public String[] getFormOfTeachingOrgs()
630    {
631        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, FORMOFTEACHING_ORG);
632    }
633
634    /**
635     * Get the teaching activity
636     * @return the teaching activity
637     */
638    public String getTeachingActivity()
639    {
640        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_ACTIVITY);
641    }
642
643    /**
644     * Get the teaching language
645     * @return the teaching language
646     */
647    public String[] getTeachingLanguage()
648    {
649        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LANGUAGE);
650    }
651    
652    /**
653     * Get the required skills
654     * @return the required skills
655     */
656    public String[] getRequiredSkills()
657    {
658        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, REQUIRED_SKILLS);
659    }
660
661    /**
662     * Get the start date
663     * @return the start date
664     */
665    public LocalDate getStartDate()
666    {
667        return getValue(START_DATE);
668    }
669
670    /**
671     * Get the time slot
672     * @return the time slot
673     */
674    public String getTimeSlot()
675    {
676        return ContentDataHelper.getContentIdFromContentData(this, TIME_SLOT);
677    }
678
679    /**
680     * Get the keywords
681     * @return the keywords
682     */
683    public String[] getKeywords()
684    {
685        return getValue(KEYWORDS, false, ArrayUtils.EMPTY_STRING_ARRAY);
686    }
687
688    /**
689     * Get the web link label
690     * @return the web link label
691     */
692    public String getWebLinkLabel()
693    {
694        return getValue(WEB_LINK_LABEL, false, StringUtils.EMPTY);
695    }
696
697    /**
698     * Get the web link URL
699     * @return the web link URL
700     */
701    public String getWebLinkUrl()
702    {
703        return getValue(WEB_LINK_URL, false, StringUtils.EMPTY);
704    }
705
706    /**
707     * Get the course type (nature)
708     * @return the course type
709     */
710    public String getCourseType()
711    {
712        return ContentDataHelper.getContentIdFromContentData(this, COURSE_TYPE);
713    }
714
715    /**
716     * Get the bibliography
717     * @return the bibliography
718     */
719    public RichText getBibliography()
720    {
721        return getValue(BIBLIOGRAPHY);
722    }
723    
724    /**
725     * Get the skills
726     * @return the skills
727     */
728    public RichText getSkills()
729    {
730        return getValue(SKILLS);
731    }
732    
733    /**
734     * Is open to exchange students
735     * @return <code>true</code> if the course is open to exchange students
736     */
737    public boolean isOpenToExchangeStudents()
738    {
739        return getValue(OPEN_TO_EXCHANGE_STUDENTS, false, false);
740    }
741    
742    // --------------------------------------------------------------------------------------//
743    // CDM-fr
744    // --------------------------------------------------------------------------------------//
745    @Override
746    public String getCDMId()
747    {
748        return "FRUAI" + _getFactory()._getRootOrgUnitRNE() + "CO" + getCode();
749    }
750    
751    @Override
752    public String getCdmCode()
753    {
754        return getValue(CDM_CODE, false, StringUtils.EMPTY);
755    }
756    
757    @Override
758    public void setCdmCode(String cdmCode)
759    {
760        setValue(CDM_CODE, cdmCode);
761    }
762}