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.impl.ModelAwareRepeater;
046import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
047import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
048import org.ametys.plugins.repository.data.holder.group.impl.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    /** Constants for syllabus attribute */
093    public static final String SYLLABUS = "syllabus";
094
095    /** Constants for additionalInformations attribute */
096    public static final String ADDITIONAL_INFORMATIONS = "additionalInformations";
097
098    /** Constants for erasmusCode attribute */
099    public static final String ERASMUS_CODE = "erasmusCode";
100
101    /** Constants for teachingLocation attribute */
102    public static final String TEACHING_LOCATION = "teachingLocation";
103
104    /** Constants for maxNumberOfStudents attribute */
105    public static final String MAX_NUMBER_OF_STUDENTS = "maxNumberOfStudents";
106
107    /** Constants for teachingTerm attribute */
108    public static final String TEACHING_TERM = "teachingTerm";
109
110    /** Constants for timeSlot attribute */
111    public static final String TIME_SLOT = "timeSlot";
112
113    /** Constants for trainingCourseDuration attribute */
114    public static final String TRAINING_COURSE_DURATION = "trainingCourseDuration";
115
116    /** Constants for teachingMethod attribute */
117    public static final String FORMODFTEACHING_METHOD = "formofteachingMethod";
118    
119    /** Constants for formofteachingOrg attribute */
120    public static final String FORMOFTEACHING_ORG = "formofteachingOrg";
121
122    /** Constants for formOfTeaching attribute */
123    public static final String TEACHING_ACTIVITY = "teachingActivity";
124
125    /** Constants for teachingLanguage attribute */
126    public static final String TEACHING_LANGUAGE = "teachingLanguage";
127
128    /** Constants for startDate attribute */
129    public static final String START_DATE = "startDate";
130
131    /** Constants for keywords attribute */
132    public static final String KEYWORDS = "keywords";
133
134    /** Constants for webLinkLabel attribute */
135    public static final String WEB_LINK_LABEL = "webLinkLabel";
136
137    /** Constants for webLinkUrl attribute */
138    public static final String WEB_LINK_URL = "webLinkUrl";
139
140    /** Constants for lomSheets attribute */
141    public static final String LOM_SHEETS = "lomSheets";
142
143    /** Constants for LOM Sheet URL attribute */
144    public static final String LOM_SHEET_URL = "linkUrl";
145    
146    /** Constants for LOM Sheet label attribute */
147    public static final String LOM_SHEET_LABEL = "linkLabel";
148    
149    /** Constants for orgunits attribute */
150    public static final String ORG_UNITS = "orgUnit";
151    
152    /** Constants for attribute 'contacts' */
153    public static final String CONTACTS = "contacts";
154    
155    /** Constants for attribute 'contacts/role' */
156    public static final String CONTACTS_ROLE = "role";
157    
158    /** Constants for attribute 'contacts/persons' */
159    public static final String CONTACTS_PERSONS = "persons";
160    
161    /** Constants for attribute 'courseType' */
162    public static final String COURSE_TYPE = "courseType";
163    
164    /** Constants for attribute 'bibliography' */
165    public static final String BIBLIOGRAPHY = "bibliography";
166
167    /** Constants for attribute 'skills' */
168    public static final String SKILLS = "skills";
169
170    /** Constants for attribute 'openToExchangeStudents' */
171    public static final String OPEN_TO_EXCHANGE_STUDENTS = "openToExchangeStudents";
172    
173    /** Constants for attribute 'courseParts' */
174    public static final String CHILD_COURSE_PARTS = "courseParts";
175
176    private String _contextPath;
177
178    /**
179     * Constructor.
180     * @param node the JCR Node.
181     * @param parentPath the parent path
182     * @param factory the corresponding factory.
183     */
184    public Course(Node node, String parentPath, CourseFactory factory)
185    {
186        super(node, parentPath, factory);
187    }
188
189    @Override
190    public List<CourseList> getCourseLists()
191    {
192        return Arrays.stream(getValue(CHILD_COURSE_LISTS, false, new ContentValue[0]))
193                .map(ContentValue::getContentIfExists)
194                .filter(Optional::isPresent)
195                .map(Optional::get)
196                .map(CourseList.class::cast)
197                .collect(Collectors.toList());
198    }
199    
200    /**
201     * Get the parent course lists
202     * @return The parent course lists
203     */
204    public List<CourseList> getParentCourseLists()
205    {
206        return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0]))
207                .map(ContentValue::getContentIfExists)
208                .filter(Optional::isPresent)
209                .map(Optional::get)
210                .map(CourseList.class::cast)
211                .collect(Collectors.toList());
212    }
213
214    @Override
215    public boolean hasCourseLists()
216    {
217        return !ContentDataHelper.isMultipleContentDataEmpty(this, CHILD_COURSE_LISTS);
218    }
219    
220    @Override
221    public boolean containsCourseList(String clId)
222    {
223        return ArrayUtils.contains(ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, CHILD_COURSE_LISTS), clId);
224    }
225    
226    @Override
227    public Set<Program> getRootPrograms()
228    {
229        return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0]))
230                .map(ContentValue::getContentIfExists)
231                .filter(Optional::isPresent)
232                .map(Optional::get)
233                .map(CourseList.class::cast)
234                .flatMap(courseList -> courseList.getRootPrograms().stream())
235                .collect(Collectors.toSet());
236    }
237    
238    // --------------------------------------------------------------------------------------//
239    // CONTACTS
240    // --------------------------------------------------------------------------------------//
241
242    /**
243     * Return the list of Persons in charge binded to this program 
244     * @return the id of contacts
245     */
246    public Set<String> getContacts()
247    {
248        Set<String> contactIds = new HashSet<>();
249        
250        ModelAwareRepeater contacts = getRepeater(CONTACTS);
251        if (contacts != null)
252        {
253            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
254            {
255                // Remove empty values
256                contactIds.addAll(ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
257                        .filter(contentId -> !contentId.isEmpty())
258                        .collect(Collectors.toSet()));
259            }
260        }
261
262        return Collections.EMPTY_SET;
263    }
264    
265    /**
266     * Return the list of Persons in charge binded to this program 
267     * @return a list of roles and UUID
268     */
269    public Map<String, List<String>> getContactsByRole()
270    {
271        Map<String, List<String>> contactsByRole = new HashMap<>();
272        
273        ModelAwareRepeater contacts = getRepeater(CONTACTS);
274        if (contacts != null)
275        {
276            for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries())
277            {
278                // Remove empty values
279                List<String> persons = ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS)
280                                                    .filter(contentId -> !contentId.isEmpty())
281                                                    .collect(Collectors.toList());
282                if (!persons.isEmpty())
283                {
284                    String role = ContentDataHelper.getContentIdFromContentData(contactEntry, CONTACTS_ROLE);
285                    contactsByRole.put(role, persons);
286                }
287            }
288        }
289
290        return contactsByRole;
291    }
292   
293    
294    // --------------------------------------------------------------------------------------//
295    // CONTEXT
296    // --------------------------------------------------------------------------------------//
297    /**
298     * Set the parent path for links and breadcrumb
299     * @param path the parent path
300     */
301    public void setContextPath (String path)
302    {
303        _contextPath = path;
304    }
305    
306    /**
307     * Get the parent path. Can be null.
308     * @return the parent path
309     */
310    public String getContextPath ()
311    {
312        return _contextPath;
313    }
314    
315    // --------------------------------------------------------------------------------------//
316    // ORGUNITS
317    // --------------------------------------------------------------------------------------//
318    /**
319     * Return the id of orgunit content binded to this course
320     * @return The id of contents
321     */
322    public List<String> getOrgUnits()
323    {
324        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS);
325    }
326
327    /**
328     * Return the id of local orgunit contents binded to this course
329     * @return The id of contents
330     */
331    public List<String> getLocalOrgUnits()
332    {
333        ValueContext localValueContext = ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL);
334        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS, localValueContext);
335    }
336
337    /**
338     * Return the id of remote orgunit contents binded to this course
339     * @return The id of contents
340     */
341    public List<String> getRemoteOrgUnits()
342    {
343        ValueContext externalValueContext = ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL);
344        return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS, externalValueContext);
345    }
346    
347    // --------------------------------------------------------------------------------------//
348    // GETTERS AND SETTERS
349    // --------------------------------------------------------------------------------------//
350    @Override
351    public String getCatalog()
352    {
353        return getValue(CATALOG);
354    }
355    
356    @Override
357    public void setCatalog(String catalog) throws AmetysRepositoryException
358    {
359        setValue(CATALOG, catalog);
360    }
361    
362    @Override
363    public String getCode()
364    {
365        return getValue(CODE, false, StringUtils.EMPTY);
366    }
367    
368    @Override
369    public void setCode(String code) throws AmetysRepositoryException
370    {
371        setValue(CODE, code);
372    }
373    
374    /**
375     * Get the description
376     * @return the description or null
377     */
378    public RichText getDescription()
379    {
380        return getValue(DESCRIPTION);
381    }
382
383    /**
384     * Get the objectives
385     * @return objectives
386     */
387    public RichText getObjectives()
388    {
389        return getValue(OBJECTIVES);
390    }
391    
392    /**
393     * Get the course parts.
394     * @return The {@link List} of attached {@link CoursePart}s
395     */
396    public List<CoursePart> getCourseParts()
397    {
398        return Arrays.stream(getValue(CHILD_COURSE_PARTS, false, new ContentValue[0]))
399                .map(ContentValue::getContentIfExists)
400                .filter(Optional::isPresent)
401                .map(Optional::get)
402                .map(CoursePart.class::cast)
403                .collect(Collectors.toList());
404    }
405    
406    /**
407     * Get the needed prerequisites
408     * @return the needed prerequisites or null if not set
409     */
410    public RichText getNeededPrerequisite()
411    {
412        return getValue(NEEDED_PREREQUISITE);
413    }
414
415    /**
416     * Get the formOfAssessment attribute
417     * @return the formOfAssessment or null if not set
418     */
419    public RichText getFormOfAssessment()
420    {
421        return getValue(FORM_OF_ASSESSMENT);
422    }
423    
424    /**
425     * Get the syllabus
426     * @return the syllabus or null if not set
427     */
428    public RichText getSyllabus()
429    {
430        return getValue(SYLLABUS);
431    }
432    
433    /**
434     * Get the list of LOM sheets
435     * @return the list of LOMsheets or an empty list
436     */
437    public Set<LOMSheet> getLOMSheets()
438    {
439        Set<LOMSheet> lomSheets = new HashSet<>();
440        
441        ModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS);
442        if (attributeLOMSheets != null)
443        {
444            for (ModelAwareRepeaterEntry attributeLOMSheet : attributeLOMSheets.getEntries())
445            {
446                LOMSheet lomSheet = new LOMSheet(attributeLOMSheet.getValue(LOM_SHEET_URL, false, StringUtils.EMPTY), attributeLOMSheet.getValue(LOM_SHEET_LABEL, false, StringUtils.EMPTY));
447                lomSheets.add(lomSheet);
448            }
449        }
450
451        return lomSheets;
452    }
453    
454    /**
455     * Set the LOM sheets
456     * @param sheets The LOM sheets
457     */
458    public void setLOMSheets (Set<LOMSheet> sheets)
459    {
460        // Remove old LOM sheets if exist
461        removeValue(LOM_SHEETS);
462        
463        ModifiableModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS, true);
464        for (LOMSheet lomSheet : sheets)
465        {
466            ModifiableModelAwareRepeaterEntry attributeLOMSheet = attributeLOMSheets.addEntry();
467            attributeLOMSheet.setValue(LOM_SHEET_URL, lomSheet.getUrl());
468            attributeLOMSheet.setValue(LOM_SHEET_LABEL, lomSheet.getLabel());
469        }
470    }
471    
472    /**
473     * Determines if the course has LOM sheet
474     * @param lomSheet The LOM sheet to test
475     * @return <code>true</code> if the course has LOM sheet
476     */
477    public boolean hasLOMSheet (LOMSheet lomSheet)
478    {
479        for (LOMSheet lom : getLOMSheets())
480        {
481            if (lom.getUrl().equals(lomSheet.getUrl()) && lom.getLabel().equals(lomSheet.getLabel()))
482            {
483                return true;
484            }
485        }
486        
487        return false;
488    }
489    
490    /**
491     * Get the additional information
492     * @return the additional information
493     */
494    public RichText getAdditionalInformations()
495    {
496        return getValue(ADDITIONAL_INFORMATIONS);
497    }
498
499    /**
500     * Get the Erasmus code
501     * @return the Erasmus code
502     */
503    public String getErasmusCode()
504    {
505        return ContentDataHelper.getContentIdFromContentData(this, ERASMUS_CODE);
506    }
507
508    /**
509     * Get the teaching location
510     * @return the teaching location
511     */
512    public String[] getTeachingLocation()
513    {
514        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LOCATION);
515    }
516
517    /**
518     * Set the teaching location
519     * @param teachingLocation the teaching location to set
520     * @throws AmetysRepositoryException if failed to set attribute
521     */
522    public void setTeachingLocation(String[] teachingLocation) throws AmetysRepositoryException
523    {
524        setValue(Course.TEACHING_LOCATION, teachingLocation);
525    }
526
527    /**
528     * Get the ECTS
529     * @return the ECTS
530     */
531    public double getEcts()
532    {
533        return getValue(ECTS, false, 0D);
534    }
535
536    /**
537     * Get the number of hours
538     * @return the number of hours
539     */
540    public double getNumberOfHours()
541    {
542        return getValue(NUMBER_OF_HOURS, false, 0D);
543    }
544
545    /**
546     * Get the effectives
547     * @return the effectives
548     * @throws AmetysRepositoryException if failed to get attribute
549     */
550    public String getMaxNumberOfStudents()
551    {
552        return getValue(MAX_NUMBER_OF_STUDENTS, false, StringUtils.EMPTY);
553    }
554
555    /**
556     * Get the teaching term
557     * @return the teaching term
558     */
559    public String getTeachingTerm()
560    {
561        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_TERM);
562    }
563
564    /**
565     * Get the level
566     * @return the level
567     */
568    public String getLevel()
569    {
570        return ContentDataHelper.getContentIdFromContentData(this, LEVEL);
571    }
572
573    /**
574     * Get the teaching method
575     * @return the teaching method
576     */
577    public String getFormOfTeachingMethod()
578    {
579        return ContentDataHelper.getContentIdFromContentData(this, FORMODFTEACHING_METHOD);
580    }
581
582    /**
583     * Get the teaching organizations
584     * @return the teaching organizations
585     */
586    public String[] getFormOfTeachingOrgs()
587    {
588        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, FORMOFTEACHING_ORG);
589    }
590
591    /**
592     * Get the teaching activity
593     * @return the teaching activity
594     */
595    public String getTeachingActivity()
596    {
597        return ContentDataHelper.getContentIdFromContentData(this, TEACHING_ACTIVITY);
598    }
599
600    /**
601     * Get the teaching language
602     * @return the teaching language
603     */
604    public String[] getTeachingLanguage()
605    {
606        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LANGUAGE);
607    }
608    
609    /**
610     * Get the required skills
611     * @return the required skills
612     */
613    public String[] getRequiredSkills()
614    {
615        return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, REQUIRED_SKILLS);
616    }
617
618    /**
619     * Get the start date
620     * @return the start date
621     */
622    public LocalDate getStartDate()
623    {
624        return getValue(START_DATE);
625    }
626
627    /**
628     * Get the time slot
629     * @return the time slot
630     */
631    public String getTimeSlot()
632    {
633        return ContentDataHelper.getContentIdFromContentData(this, TIME_SLOT);
634    }
635
636    /**
637     * Get the keywords
638     * @return the keywords
639     */
640    public String[] getKeywords()
641    {
642        return getValue(KEYWORDS, false, ArrayUtils.EMPTY_STRING_ARRAY);
643    }
644
645    /**
646     * Get the web link label
647     * @return the web link label
648     */
649    public String getWebLinkLabel()
650    {
651        return getValue(WEB_LINK_LABEL, false, StringUtils.EMPTY);
652    }
653
654    /**
655     * Get the web link URL
656     * @return the web link URL
657     */
658    public String getWebLinkUrl()
659    {
660        return getValue(WEB_LINK_URL, false, StringUtils.EMPTY);
661    }
662
663    /**
664     * Get the course type (nature)
665     * @return the course type
666     */
667    public String getCourseType()
668    {
669        return ContentDataHelper.getContentIdFromContentData(this, COURSE_TYPE);
670    }
671
672    /**
673     * Get the bibliography
674     * @return the bibliography
675     */
676    public RichText getBibliography()
677    {
678        return getValue(BIBLIOGRAPHY);
679    }
680    
681    /**
682     * Get the skills
683     * @return the skills
684     */
685    public RichText getSkills()
686    {
687        return getValue(SKILLS);
688    }
689    
690    /**
691     * Is open to exchange students
692     * @return <code>true</code> if the course is open to exchange students
693     */
694    public boolean isOpenToExchangeStudents()
695    {
696        return getValue(OPEN_TO_EXCHANGE_STUDENTS, false, false);
697    }
698    
699    // --------------------------------------------------------------------------------------//
700    // CDM-fr
701    // --------------------------------------------------------------------------------------//
702    @Override
703    public String getCDMId()
704    {
705        return "FRUAI" + _getFactory()._getRootOrgUnitRNE() + "CO" + getCode();
706    }
707    
708    @Override
709    public String getCdmCode()
710    {
711        return getValue(CDM_CODE, false, StringUtils.EMPTY);
712    }
713    
714    @Override
715    public void setCdmCode(String cdmCode) 
716    {
717        setValue(CDM_CODE, cdmCode);
718    }
719}