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