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