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