001/* 002 * Copyright 2019 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.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Objects; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.configuration.Configurable; 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.commons.collections4.CollectionUtils; 036import org.apache.commons.lang.ArrayUtils; 037import org.apache.commons.lang3.StringUtils; 038 039import org.ametys.core.user.UserIdentity; 040import org.ametys.odf.ODFHelper; 041import org.ametys.odf.course.ShareableCourseStatusHelper.ShareableStatus; 042import org.ametys.odf.courselist.CourseList; 043import org.ametys.odf.enumeration.OdfReferenceTableEntry; 044import org.ametys.odf.enumeration.OdfReferenceTableHelper; 045import org.ametys.odf.program.Container; 046import org.ametys.odf.program.Program; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051 052import com.beust.jcommander.internal.Lists; 053import com.beust.jcommander.internal.Sets; 054 055/** 056 * Helper for shareable course 057 */ 058public class ShareableCourseHelper extends AbstractLogEnabled implements Component, Serviceable, Configurable 059{ 060 /** The component role. */ 061 public static final String ROLE = ShareableCourseHelper.class.getName(); 062 063 /** The metadata name to know if the course is shareable */ 064 public static final String SHAREABLE_METADATA_NAME = "shareable"; 065 066 /** The metadata name for programs field */ 067 public static final String PROGRAMS_FIELD_METADATA_NAME = "shareable-programs"; 068 069 /** The metadata name for periods field */ 070 public static final String PERIODS_FIELD_METADATA_NAME = "shareable-periods"; 071 072 /** The metadata name for degrees field */ 073 public static final String DEGREES_FIELD_METADATA_NAME = "shareable-degrees"; 074 075 /** The metadata name for orgunits field */ 076 public static final String ORGUNITS_FIELD_METADATA_NAME = "shareable-orgunits"; 077 078 private static final String __OWN_KEY = "OWN"; 079 080 /** The shareable course configuration */ 081 protected ShareableConfiguration _shareableCourseConfiguration; 082 083 /** The ODF reference table helper */ 084 protected OdfReferenceTableHelper _odfRefTableHelper; 085 086 /** The shareable course status */ 087 protected ShareableCourseStatusHelper _shareableCourseStatus; 088 089 /** The ODF helper */ 090 protected ODFHelper _odfHelper; 091 092 /** The Ametys object resolver */ 093 protected AmetysObjectResolver _resolver; 094 095 public void service(ServiceManager manager) throws ServiceException 096 { 097 _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 098 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 099 _shareableCourseStatus = (ShareableCourseStatusHelper) manager.lookup(ShareableCourseStatusHelper.ROLE); 100 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 101 } 102 103 public void configure(Configuration configuration) throws ConfigurationException 104 { 105 if ("shareable-fields".equals(configuration.getName())) 106 { 107 _shareableCourseConfiguration = _parseConfiguration(configuration); 108 } 109 else 110 { 111 _shareableCourseConfiguration = _parseConfiguration(configuration.getChild("shareable-fields")); 112 } 113 114 } 115 116 /** 117 * Parse configuration to create a shareable course configuration 118 * @param configuration the configuration 119 * @return the shareable course configuration 120 */ 121 protected ShareableConfiguration _parseConfiguration(Configuration configuration) 122 { 123 boolean autoValidated = _parseAutoValidated(configuration); 124 boolean isShareable = _parseShareableField(configuration); 125 126 ShareableField programField = _parseProgramField(configuration); 127 ShareableField degreeField = _parseDegreeField(configuration); 128 ShareableField periodField = _parsePeriodField(configuration); 129 ShareableField orgUnitField = _parseOrgUnitField(configuration); 130 131 return new ShareableConfiguration(isShareable, programField, degreeField, orgUnitField, periodField, autoValidated); 132 } 133 134 135 /** 136 * Parse configuration to know if courses are validated after initialization 137 * @param configuration the configuration 138 * @return <code>true</code> if courses are validated after initialization 139 */ 140 protected boolean _parseAutoValidated(Configuration configuration) 141 { 142 return Boolean.valueOf(configuration.getAttribute("auto-validated", "false")); 143 } 144 145 146 /** 147 * Parse configuration to know if courses are shareable 148 * @param configuration the configuration 149 * @return <code>true</code> if courses are shareable 150 */ 151 protected boolean _parseShareableField(Configuration configuration) 152 { 153 Configuration shareableConf = configuration.getChild(SHAREABLE_METADATA_NAME); 154 return Boolean.parseBoolean(shareableConf.getValue("false")); 155 } 156 157 /** 158 * Parse configuration to get program field 159 * @param configuration the configuration 160 * @return the program field 161 */ 162 protected ShareableField _parseProgramField(Configuration configuration) 163 { 164 Configuration programsConf = configuration.getChild(PROGRAMS_FIELD_METADATA_NAME); 165 String programValuesAsString = programsConf.getValue(StringUtils.EMPTY); 166 if (__OWN_KEY.equals(programValuesAsString)) 167 { 168 return new ShareableField(true, Sets.newHashSet()); 169 } 170 else 171 { 172 List<String> valuesAsList = Arrays.asList(StringUtils.split(programValuesAsString, ",")); 173 Set<String> programValues = valuesAsList.stream() 174 .map(StringUtils::trim) 175 .filter(StringUtils::isNotBlank) 176 .filter(this::_isContentExist) 177 .collect(Collectors.toSet()); 178 179 return new ShareableField(false, programValues); 180 } 181 } 182 183 /** 184 * Parse configuration to get degree field 185 * @param configuration the configuration 186 * @return the degree field 187 */ 188 protected ShareableField _parseDegreeField(Configuration configuration) 189 { 190 Configuration degreesConf = configuration.getChild(DEGREES_FIELD_METADATA_NAME); 191 String degreeValuesAsString = degreesConf.getValue(StringUtils.EMPTY); 192 if (__OWN_KEY.equals(degreeValuesAsString)) 193 { 194 return new ShareableField(true, Sets.newHashSet()); 195 } 196 else 197 { 198 List<String> valuesAsList = Arrays.asList(StringUtils.split(degreeValuesAsString, ",")); 199 Set<String> degreeValues = valuesAsList.stream() 200 .map(StringUtils::trim) 201 .filter(StringUtils::isNotBlank) 202 .map(d -> _getItemIdFromCDM(d, OdfReferenceTableHelper.DEGREE)) 203 .filter(Objects::nonNull) 204 .collect(Collectors.toSet()); 205 206 return new ShareableField(false, degreeValues); 207 } 208 } 209 210 /** 211 * Parse configuration to get period field 212 * @param configuration the configuration 213 * @return the period field 214 */ 215 protected ShareableField _parsePeriodField(Configuration configuration) 216 { 217 Configuration periodsConf = configuration.getChild(PERIODS_FIELD_METADATA_NAME); 218 String periodValuesAsString = periodsConf.getValue(StringUtils.EMPTY); 219 if (__OWN_KEY.equals(periodValuesAsString)) 220 { 221 return new ShareableField(true, Sets.newHashSet()); 222 } 223 else 224 { 225 List<String> valuesAsList = Arrays.asList(StringUtils.split(periodValuesAsString, ",")); 226 Set<String> periodValues = valuesAsList.stream() 227 .map(StringUtils::trim) 228 .filter(StringUtils::isNotBlank) 229 .map(p -> _getItemIdFromCDM(p, OdfReferenceTableHelper.PERIOD)) 230 .filter(Objects::nonNull) 231 .collect(Collectors.toSet()); 232 233 return new ShareableField(false, periodValues); 234 } 235 } 236 237 /** 238 * Parse configuration to get orgUnit field 239 * @param configuration the configuration 240 * @return the orgUnit field 241 */ 242 protected ShareableField _parseOrgUnitField(Configuration configuration) 243 { 244 Configuration orgUnitsConf = configuration.getChild(ORGUNITS_FIELD_METADATA_NAME); 245 String orgUnitValuesAsString = orgUnitsConf.getValue(StringUtils.EMPTY); 246 if (__OWN_KEY.equals(orgUnitValuesAsString)) 247 { 248 return new ShareableField(true, Sets.newHashSet()); 249 } 250 else 251 { 252 List<String> valuesAsList = Arrays.asList(StringUtils.split(orgUnitValuesAsString, ",")); 253 Set<String> orgUnitValues = valuesAsList.stream() 254 .map(StringUtils::trim) 255 .filter(StringUtils::isNotBlank) 256 .filter(this::_isContentExist) 257 .collect(Collectors.toSet()); 258 259 return new ShareableField(false, orgUnitValues); 260 } 261 } 262 263 private String _getItemIdFromCDM(String cdmValue, String contentType) 264 { 265 OdfReferenceTableEntry content = _odfRefTableHelper.getItemFromCDM(contentType, cdmValue); 266 if (content == null) 267 { 268 getLogger().warn("Find a wrong data in the shareable configuration : can't get content from cdmValue {}", cdmValue); 269 return null; 270 } 271 272 return content.getId(); 273 } 274 275 private boolean _isContentExist(String contentId) 276 { 277 try 278 { 279 _resolver.resolveById(contentId); 280 return true; 281 } 282 catch (Exception e) 283 { 284 getLogger().warn("Find a wrong data in the shareable configuration : can't get content from id {}", contentId); 285 return false; 286 } 287 } 288 289 /** 290 * Initialize the shareable course fields 291 * @param courseContent the course content to initialize 292 * @param courseListContent the course list parent. Can be null 293 * @param user the user who initialize the shareable fields 294 * @param ignoreRights true to ignore user rights 295 * @return <code>true</code> if there are changes 296 */ 297 public boolean initializeShareableFields(Course courseContent, CourseList courseListContent, UserIdentity user, boolean ignoreRights) 298 { 299 List<CourseList> courseListContents = courseListContent != null ? Collections.singletonList(courseListContent) : Lists.newArrayList(); 300 return initializeShareableFields(courseContent, courseListContents, user, ignoreRights); 301 } 302 303 /** 304 * Initialize the shareable course fields 305 * @param courseContent the course content to initialize 306 * @param courseListContents the list of course list parents. Can be empty 307 * @param user the user who initialize the shareable fields 308 * @param ignoreRights true to ignore user rights 309 * @return <code>true</code> if there are changes 310 */ 311 public boolean initializeShareableFields(Course courseContent, List<CourseList> courseListContents, UserIdentity user, boolean ignoreRights) 312 { 313 boolean hasChanges = false; 314 if (handleShareableCourse()) 315 { 316 ModifiableCompositeMetadata metadataHolder = courseContent.getMetadataHolder(); 317 courseContent.setValue(SHAREABLE_METADATA_NAME, _shareableCourseConfiguration.isShareable()); 318 hasChanges = true; 319 320 Set<Program> parentPrograms = new HashSet<>(); 321 Set<Container> parentContainers = new HashSet<>(); 322 for (CourseList courseList : courseListContents) 323 { 324 parentPrograms.addAll(_odfHelper.getParentPrograms(courseList)); 325 parentContainers.addAll(_odfHelper.getParentContainers(courseList)); 326 } 327 328 ShareableField programField = _shareableCourseConfiguration.getProgramField(); 329 Set<String> programs = programField.ownContext() ? getProgramIds(parentPrograms) : programField.getDefaultValues(); 330 if (!programs.isEmpty()) 331 { 332 metadataHolder.setMetadata(PROGRAMS_FIELD_METADATA_NAME, programs.toArray(new String[programs.size()])); 333 } 334 335 ShareableField degreeField = _shareableCourseConfiguration.getDegreeField(); 336 Set<String> degrees = degreeField.ownContext() ? getDegrees(parentPrograms) : degreeField.getDefaultValues(); 337 if (!degrees.isEmpty()) 338 { 339 metadataHolder.setMetadata(DEGREES_FIELD_METADATA_NAME, degrees.toArray(new String[degrees.size()])); 340 } 341 342 ShareableField periodField = _shareableCourseConfiguration.getPeriodField(); 343 Set<String> periods = periodField.ownContext() ? getPeriods(parentContainers) : periodField.getDefaultValues(); 344 if (!periods.isEmpty()) 345 { 346 metadataHolder.setMetadata(PERIODS_FIELD_METADATA_NAME, periods.toArray(new String[periods.size()])); 347 } 348 349 ShareableField orgUnitField = _shareableCourseConfiguration.getOrgUnitField(); 350 Set<String> orgUnits = orgUnitField.ownContext() ? getOrgUnits(parentPrograms) : orgUnitField.getDefaultValues(); 351 if (!orgUnits.isEmpty()) 352 { 353 metadataHolder.setMetadata(ORGUNITS_FIELD_METADATA_NAME, orgUnits.toArray(new String[orgUnits.size()])); 354 } 355 356 if (_shareableCourseConfiguration.autoValidated()) 357 { 358 _shareableCourseStatus.setWorkflowStateAttribute(courseContent, LocalDate.now(), user, ShareableStatus.VALIDATED, ignoreRights); 359 } 360 } 361 362 return hasChanges; 363 } 364 365 /** 366 * True if shareable course fields match with the course list 367 * @param courseContent the shareable course 368 * @param courseList the course list 369 * @return <code>true</code> if shareable course fields match with the course list 370 */ 371 public boolean isShareableFieldsMatch(Course courseContent, CourseList courseList) 372 { 373 if (!isShareableCourse(courseContent) || _shareableCourseStatus.getShareableStatus(courseContent) != ShareableStatus.VALIDATED) 374 { 375 return false; 376 } 377 378 List<String> programValues = getFieldValues(courseContent, PROGRAMS_FIELD_METADATA_NAME); 379 List<String> degreeValues = getFieldValues(courseContent, DEGREES_FIELD_METADATA_NAME); 380 List<String> orgUnitValues = getFieldValues(courseContent, ORGUNITS_FIELD_METADATA_NAME); 381 List<String> periodValues = getFieldValues(courseContent, PERIODS_FIELD_METADATA_NAME); 382 383 if (programValues.isEmpty() && degreeValues.isEmpty() && orgUnitValues.isEmpty() && periodValues.isEmpty()) 384 { 385 return true; 386 } 387 388 boolean isSharedWith = true; 389 if (!periodValues.isEmpty()) 390 { 391 Set<Container> parentContainers = _odfHelper.getParentContainers(courseList); 392 Set<String> courseListPeriods = getPeriods(parentContainers); 393 394 isSharedWith = CollectionUtils.containsAny(courseListPeriods, periodValues) && isSharedWith; 395 } 396 397 if (isSharedWith) 398 { 399 Set<Program> parentPrograms = _odfHelper.getParentPrograms(courseList); 400 401 if (!programValues.isEmpty()) 402 { 403 Set<String> courseListprogramIds = getProgramIds(parentPrograms); 404 isSharedWith = CollectionUtils.containsAny(courseListprogramIds, programValues); 405 } 406 407 if (!degreeValues.isEmpty()) 408 { 409 Set<String> courseListDegrees = getDegrees(parentPrograms); 410 isSharedWith = isSharedWith && CollectionUtils.containsAny(courseListDegrees, degreeValues); 411 } 412 413 if (!orgUnitValues.isEmpty()) 414 { 415 Set<String> courseListorgUnitIds = getOrgUnits(parentPrograms); 416 isSharedWith = isSharedWith && CollectionUtils.containsAny(courseListorgUnitIds, orgUnitValues); 417 } 418 } 419 420 return isSharedWith; 421 } 422 423 /** 424 * Get field value 425 * @param courseContent the course content 426 * @param fieldMetadataName the name of the field 427 * @return the list of values 428 */ 429 public List<String> getFieldValues(Course courseContent, String fieldMetadataName) 430 { 431 ModifiableCompositeMetadata metadataHolder = courseContent.getMetadataHolder(); 432 String[] values = metadataHolder.getStringArray(fieldMetadataName, ArrayUtils.EMPTY_STRING_ARRAY); 433 434 return Arrays.asList(values); 435 } 436 437 /** 438 * True if the course content is shareable 439 * @param courseContent the course content 440 * @return <code>true</code> if the course is shareable 441 */ 442 public boolean isShareableCourse(Course courseContent) 443 { 444 if (courseContent.hasValue(SHAREABLE_METADATA_NAME)) 445 { 446 return courseContent.getValue(SHAREABLE_METADATA_NAME, false, false); 447 } 448 449 return false; 450 } 451 452 /** 453 * Get programs ids from programs 454 * @param programs the list of programs 455 * @return the list of progam ids 456 */ 457 public Set<String> getProgramIds(Set<Program> programs) 458 { 459 return programs.stream() 460 .map(Program::getId) 461 .collect(Collectors.toSet()); 462 } 463 464 /** 465 * Get degrees from programs 466 * @param programs the list of programs 467 * @return the list of degrees 468 */ 469 public Set<String> getDegrees(Set<Program> programs) 470 { 471 return programs.stream() 472 .map(Program::getDegree) 473 .filter(StringUtils::isNotBlank) 474 .collect(Collectors.toSet()); 475 } 476 477 /** 478 * Get periods from containers 479 * @param containers the list of containers 480 * @return the list of periods 481 */ 482 public Set<String> getPeriods(Set<Container> containers) 483 { 484 return containers.stream() 485 .map(Container::getPeriod) 486 .filter(StringUtils::isNotBlank) 487 .collect(Collectors.toSet()); 488 } 489 490 /** 491 * Get orgUnits from programs 492 * @param programs the list of programs 493 * @return the list of orgUnits 494 */ 495 public Set<String> getOrgUnits(Set<Program> programs) 496 { 497 return programs.stream() 498 .map(Program::getOrgUnits) 499 .flatMap(Collection::stream) 500 .filter(StringUtils::isNotBlank) 501 .collect(Collectors.toSet()); 502 } 503 504 /** 505 * Return true if shareable course are handled 506 * @return <code>true</code> if shareable course are handled 507 */ 508 public boolean handleShareableCourse() 509 { 510 boolean handleShareableCourse = Config.getInstance().getValue("odf.shareable.course.enable"); 511 return handleShareableCourse; 512 } 513 514 private static class ShareableField 515 { 516 private boolean _own; 517 private Set<String> _values; 518 519 public ShareableField(boolean own, Set<String> values) 520 { 521 _own = own; 522 _values = values; 523 } 524 525 public boolean ownContext() 526 { 527 return _own; 528 } 529 530 public Set<String> getDefaultValues() 531 { 532 return _values; 533 } 534 } 535 536 private static class ShareableConfiguration 537 { 538 private boolean _isShareable; 539 private ShareableField _programField; 540 private ShareableField _degreeField; 541 private ShareableField _orgUnitField; 542 private ShareableField _periodField; 543 private boolean _autoValidated; 544 545 public ShareableConfiguration(boolean isShareable, ShareableField programField, ShareableField degreeField, ShareableField orgUnitField, ShareableField periodField, boolean autoValidated) 546 { 547 _isShareable = isShareable; 548 _programField = programField; 549 _degreeField = degreeField; 550 _orgUnitField = orgUnitField; 551 _periodField = periodField; 552 _autoValidated = autoValidated; 553 } 554 555 public boolean autoValidated() 556 { 557 return _autoValidated; 558 } 559 560 public boolean isShareable() 561 { 562 return _isShareable; 563 } 564 565 public ShareableField getProgramField() 566 { 567 return _programField; 568 } 569 570 public ShareableField getDegreeField() 571 { 572 return _degreeField; 573 } 574 575 public ShareableField getOrgUnitField() 576 { 577 return _orgUnitField; 578 } 579 580 public ShareableField getPeriodField() 581 { 582 return _periodField; 583 } 584 } 585 586}