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