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