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.stream.Collectors;
028
029import javax.jcr.Node;
030
031import org.apache.commons.lang3.ArrayUtils;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.data.ContentDataHelper;
035import org.ametys.cms.data.ContentValue;
036import org.ametys.cms.data.RichText;
037import org.ametys.cms.repository.ModifiableDefaultContent;
038import org.ametys.odf.ProgramItem;
039import org.ametys.odf.courselist.CourseList;
040import org.ametys.odf.courselist.CourseListContainer;
041import org.ametys.odf.coursepart.CoursePart;
042import org.ametys.odf.program.Program;
043import org.ametys.plugins.repository.AmetysRepositoryException;
044import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
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.plugins.repository.data.holder.values.ValueContext;
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 level attribute */
069    public static final String LEVEL = "level";
070
071    /** Constants for description attribute */
072    public static final String DESCRIPTION = "description";
073
074    /** Constants for objectives attribute */
075    public static final String OBJECTIVES = "objectives";
076
077    /** Constants for nbHours attribute */
078    public static final String NUMBER_OF_HOURS = "nbHours";
079
080    /** Constants for neededPrerequisite attribute */
081    public static final String NEEDED_PREREQUISITE = "neededPrerequisite";
082
083    /** Constants for formOfAssessment attribute */
084    public static final String FORM_OF_ASSESSMENT = "formOfAssessment";
085    
086    /** Constants for attribute 'requiredSkills' */
087    public static final String REQUIRED_SKILLS = "requiredSkills";
088    
089    /** Constants for repeater 'acquiredSkills' */
090    public static final String ACQUIRED_SKILLS = "acquiredSkills";
091
092    /** The attribute name of the skills */
093    public static final String ACQUIRED_SKILLS_SKILLSET = "skillSet";
094    
095    /** The attribute name of the skills in acquiredSkills */
096    public static final String ACQUIRED_SKILLS_SKILLS = "skills";
097
098    /** The skill attribute name in acquiredSkills/skills */
099    public static final String ACQUIRED_SKILLS_SKILLS_SKILL = "skill";
100    
101    /** The skill acquisition level attribute name in acquiredSkills/skills */
102    public static final String ACQUIRED_SKILLS_SKILLS_ACQUISITION_LEVEL = "acquisitionLevel";
103
104    /** Constants for syllabus attribute */
105    public static final String SYLLABUS = "syllabus";
106
107    /** Constants for additionalInformations attribute */
108    public static final String ADDITIONAL_INFORMATIONS = "additionalInformations";
109
110    /** Constants for erasmusCode attribute */
111    public static final String ERASMUS_CODE = "erasmusCode";
112
113    /** Constants for teachingLocation attribute */
114    public static final String TEACHING_LOCATION = "teachingLocation";
115
116    /** Constants for maxNumberOfStudents attribute */
117    public static final String MAX_NUMBER_OF_STUDENTS = "maxNumberOfStudents";
118
119    /** Constants for teachingTerm attribute */
120    public static final String TEACHING_TERM = "teachingTerm";
121
122    /** Constants for timeSlot attribute */
123    public static final String TIME_SLOT = "timeSlot";
124
125    /** Constants for trainingCourseDuration attribute */
126    public static final String TRAINING_COURSE_DURATION = "trainingCourseDuration";
127
128    /** Constants for teachingMethod attribute */
129    public static final String FORMODFTEACHING_METHOD = "formofteachingMethod";
130    
131    /** Constants for formofteachingOrg attribute */
132    public static final String FORMOFTEACHING_ORG = "formofteachingOrg";
133
134    /** Constants for formOfTeaching attribute */
135    public static final String TEACHING_ACTIVITY = "teachingActivity";
136
137    /** Constants for teachingLanguage attribute */
138    public static final String TEACHING_LANGUAGE = "teachingLanguage";
139
140    /** Constants for startDate attribute */
141    public static final String START_DATE = "startDate";
142
143    /** Constants for keywords attribute */
144    public static final String KEYWORDS = "keywords";
145
146    /** Constants for webLinkLabel attribute */
147    public static final String WEB_LINK_LABEL = "webLinkLabel";
148
149    /** Constants for webLinkUrl attribute */
150    public static final String WEB_LINK_URL = "webLinkUrl";
151
152    /** Constants for lomSheets attribute */
153    public static final String LOM_SHEETS = "lomSheets";
154
155    /** Constants for LOM Sheet URL attribute */
156    public static final String LOM_SHEET_URL = "linkUrl";
157    
158    /** Constants for LOM Sheet label attribute */
159    public static final String LOM_SHEET_LABEL = "linkLabel";
160    
161    /** Constants for orgunits attribute */
162    public static final String ORG_UNITS = "orgUnit";
163    
164    /** Constants for attribute 'contacts' */
165    public static final String CONTACTS = "contacts";
166    
167    /** Constants for attribute 'contacts/role' */
168    public static final String CONTACTS_ROLE = "role";
169    
170    /** Constants for attribute 'contacts/persons' */
171    public static final String CONTACTS_PERSONS = "persons";
172    
173    /** Constants for attribute 'courseType' */
174    public static final String COURSE_TYPE = "courseType";
175    
176    /** Constants for attribute 'bibliography' */
177    public static final String BIBLIOGRAPHY = "bibliography";
178
179    /** Constants for attribute 'skills' */
180    public static final String SKILLS = "skills";
181
182    /** Constants for attribute 'openToExchangeStudents' */
183    public static final String OPEN_TO_EXCHANGE_STUDENTS = "openToExchangeStudents";
184    
185    /** Constants for attribute 'courseParts' */
186    public static final String CHILD_COURSE_PARTS = "courseParts";
187
188    private String _contextPath;
189
190    /**
191     * Constructor.
192     * @param node the JCR Node.
193     * @param parentPath the parent path
194     * @param factory the corresponding factory.
195     */
196    public Course(Node node, String parentPath, CourseFactory factory)
197    {
198        super(node, parentPath, factory);
199    }
200
201    @Override
202    public List<CourseList> getCourseLists()
203    {
204        return Arrays.stream(getValue(CHILD_COURSE_LISTS, false, new ContentValue[0]))
205                .map(ContentValue::getContentIfExists)
206                .filter(Optional::isPresent)
207                .map(Optional::get)
208                .map(CourseList.class::cast)
209                .collect(Collectors.toList());
210    }
211    
212    /**
213     * Get the parent course lists
214     * @return The parent course lists
215     */
216    public List<CourseList> getParentCourseLists()
217    {
218        return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0]))
219                .map(ContentValue::getContentIfExists)
220                .filter(Optional::isPresent)
221                .map(Optional::get)
222                .map(CourseList.class::cast)
223                .collect(Collectors.toList());
224    }
225
226    @Override
227    public boolean hasCourseLists()
228    {
229        return !ContentDataHelper.isMultipleContentDataEmpty(this, CHILD_COURSE_LISTS);
230    }
231    
232    @Override
233    public boolean containsCourseList(String clId)
234    {
235        return ArrayUtils.contains(ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, CHILD_COURSE_LISTS), clId);
236    }
237    
238    @Override
239    public Set<Program> getRootPrograms()
240    {
241        return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0]))
242                .map(ContentValue::getContentIfExists)
243                .filter(Optional::isPresent)
244                .map(Optional::get)
245                .map(CourseList.class::cast)
246                .flatMap(courseList -> courseList.getRootPrograms().stream())
247                .collect(Collectors.toSet());
248    }
249    
250    // --------------------------------------------------------------------------------------//
251    // CONTACTS
252    // --------------------------------------------------------------------------------------//
253
254    /**
255     * Return the list of Persons in charge binded to this program 
256     * @return the id of contacts
257     */
258    public Set<String> getContacts()
259    {
260        Set<String> contactIds = new HashSet<>();
261        
262        ModelAwareRepeater contacts = getRepeater(CONTACTS);
263        if (contacts != null)
264        {
265            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
266            {
267                // Remove empty values
268                contactIds.addAll(ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
269                        .filter(contentId -> !contentId.isEmpty())
270                        .collect(Collectors.toSet()));
271            }
272        }
273
274        return Collections.EMPTY_SET;
275    }
276    
277    /**
278     * Return the list of Persons in charge binded to this program 
279     * @return a list of roles and UUID
280     */
281    public Map<String, List<String>> getContactsByRole()
282    {
283        Map<String, List<String>> contactsByRole = new HashMap<>();
284        
285        ModelAwareRepeater contacts = getRepeater(CONTACTS);
286        if (contacts != null)
287        {
288            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
289            {
290                // Remove empty values
291                List<String> persons = ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
292                                                    .filter(contentId -> !contentId.isEmpty())
293                                                    .collect(Collectors.toList());
294                if (!persons.isEmpty())
295                {
296                    String role = ContentDataHelper.getContentIdFromContentData(contactEntry, CONTACTS_ROLE);
297                    contactsByRole.put(role, persons);
298                }
299            }
300        }
301
302        return contactsByRole;
303    }
304   
305    
306    // --------------------------------------------------------------------------------------//
307    // CONTEXT
308    // --------------------------------------------------------------------------------------//
309    /**
310     * Set the parent path for links and breadcrumb
311     * @param path the parent path
312     */
313    public void setContextPath (String path)
314    {
315        _contextPath = path;
316    }
317    
318    /**
319     * Get the parent path. Can be null.
320     * @return the parent path
321     */
322    public String getContextPath ()
323    {
324        return _contextPath;
325    }
326    
327    // --------------------------------------------------------------------------------------//
328    // ORGUNITS
329    // --------------------------------------------------------------------------------------//
330    /**
331     * Return the id of orgunit content binded to this course
332     * @return The id of contents
333     */
334    public List<String> getOrgUnits()
335    {
336        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS);
337    }
338
339    /**
340     * Return the id of local orgunit contents binded to this course
341     * @return The id of contents
342     */
343    public List<String> getLocalOrgUnits()
344    {
345        ValueContext localValueContext = ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL);
346        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS, localValueContext);
347    }
348
349    /**
350     * Return the id of remote orgunit contents binded to this course
351     * @return The id of contents
352     */
353    public List<String> getRemoteOrgUnits()
354    {
355        ValueContext externalValueContext = ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL);
356        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS, externalValueContext);
357    }
358    
359    // --------------------------------------------------------------------------------------//
360    // GETTERS AND SETTERS
361    // --------------------------------------------------------------------------------------//
362    @Override
363    public String getCatalog()
364    {
365        return getValue(CATALOG);
366    }
367    
368    @Override
369    public void setCatalog(String catalog) throws AmetysRepositoryException
370    {
371        setValue(CATALOG, catalog);
372    }
373    
374    @Override
375    public String getCode()
376    {
377        return getValue(CODE, false, StringUtils.EMPTY);
378    }
379    
380    @Override
381    public void setCode(String code) throws AmetysRepositoryException
382    {
383        setValue(CODE, code);
384    }
385    
386    /**
387     * Get the description
388     * @return the description or null
389     */
390    public RichText getDescription()
391    {
392        return getValue(DESCRIPTION);
393    }
394
395    /**
396     * Get the objectives
397     * @return objectives
398     */
399    public RichText getObjectives()
400    {
401        return getValue(OBJECTIVES);
402    }
403    
404    /**
405     * Get the course parts.
406     * @return The {@link List} of attached {@link CoursePart}s
407     */
408    public List<CoursePart> getCourseParts()
409    {
410        return Arrays.stream(getValue(CHILD_COURSE_PARTS, false, new ContentValue[0]))
411                .map(ContentValue::getContentIfExists)
412                .filter(Optional::isPresent)
413                .map(Optional::get)
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     * Get the number of hours
550     * @return the number of hours
551     */
552    public double getNumberOfHours()
553    {
554        return getValue(NUMBER_OF_HOURS, false, 0D);
555    }
556
557    /**
558     * Get the effectives
559     * @return the effectives
560     * @throws AmetysRepositoryException if failed to get attribute
561     */
562    public String getMaxNumberOfStudents()
563    {
564        return getValue(MAX_NUMBER_OF_STUDENTS, false, StringUtils.EMPTY);
565    }
566
567    /**
568     * Get the teaching term
569     * @return the teaching term
570     */
571    public String getTeachingTerm()
572    {
573        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_TERM);
574    }
575
576    /**
577     * Get the level
578     * @return the level
579     */
580    public String getLevel()
581    {
582        return ContentDataHelper.getContentIdFromContentData(this, LEVEL);
583    }
584
585    /**
586     * Get the teaching method
587     * @return the teaching method
588     */
589    public String getFormOfTeachingMethod()
590    {
591        return ContentDataHelper.getContentIdFromContentData(this, FORMODFTEACHING_METHOD);
592    }
593
594    /**
595     * Get the teaching organizations
596     * @return the teaching organizations
597     */
598    public String[] getFormOfTeachingOrgs()
599    {
600        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, FORMOFTEACHING_ORG);
601    }
602
603    /**
604     * Get the teaching activity
605     * @return the teaching activity
606     */
607    public String getTeachingActivity()
608    {
609        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_ACTIVITY);
610    }
611
612    /**
613     * Get the teaching language
614     * @return the teaching language
615     */
616    public String[] getTeachingLanguage()
617    {
618        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LANGUAGE);
619    }
620    
621    /**
622     * Get the required skills
623     * @return the required skills
624     */
625    public String[] getRequiredSkills()
626    {
627        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, REQUIRED_SKILLS);
628    }
629
630    /**
631     * Get the start date
632     * @return the start date
633     */
634    public LocalDate getStartDate()
635    {
636        return getValue(START_DATE);
637    }
638
639    /**
640     * Get the time slot
641     * @return the time slot
642     */
643    public String getTimeSlot()
644    {
645        return ContentDataHelper.getContentIdFromContentData(this, TIME_SLOT);
646    }
647
648    /**
649     * Get the keywords
650     * @return the keywords
651     */
652    public String[] getKeywords()
653    {
654        return getValue(KEYWORDS, false, ArrayUtils.EMPTY_STRING_ARRAY);
655    }
656
657    /**
658     * Get the web link label
659     * @return the web link label
660     */
661    public String getWebLinkLabel()
662    {
663        return getValue(WEB_LINK_LABEL, false, StringUtils.EMPTY);
664    }
665
666    /**
667     * Get the web link URL
668     * @return the web link URL
669     */
670    public String getWebLinkUrl()
671    {
672        return getValue(WEB_LINK_URL, false, StringUtils.EMPTY);
673    }
674
675    /**
676     * Get the course type (nature)
677     * @return the course type
678     */
679    public String getCourseType()
680    {
681        return ContentDataHelper.getContentIdFromContentData(this, COURSE_TYPE);
682    }
683
684    /**
685     * Get the bibliography
686     * @return the bibliography
687     */
688    public RichText getBibliography()
689    {
690        return getValue(BIBLIOGRAPHY);
691    }
692    
693    /**
694     * Get the skills
695     * @return the skills
696     */
697    public RichText getSkills()
698    {
699        return getValue(SKILLS);
700    }
701    
702    /**
703     * Is open to exchange students
704     * @return <code>true</code> if the course is open to exchange students
705     */
706    public boolean isOpenToExchangeStudents()
707    {
708        return getValue(OPEN_TO_EXCHANGE_STUDENTS, false, false);
709    }
710    
711    // --------------------------------------------------------------------------------------//
712    // CDM-fr
713    // --------------------------------------------------------------------------------------//
714    @Override
715    public String getCDMId()
716    {
717        return "FRUAI" + _getFactory()._getRootOrgUnitRNE() + "CO" + getCode();
718    }
719    
720    @Override
721    public String getCdmCode()
722    {
723        return getValue(CDM_CODE, false, StringUtils.EMPTY);
724    }
725    
726    @Override
727    public void setCdmCode(String cdmCode) 
728    {
729        setValue(CDM_CODE, cdmCode);
730    }
731}