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.migration; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import javax.jcr.RepositoryException; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.commons.lang3.ArrayUtils; 033import org.apache.commons.lang3.tuple.Pair; 034 035import org.ametys.cms.CmsConstants; 036import org.ametys.cms.FilterNameHelper; 037import org.ametys.cms.data.ContentDataHelper; 038import org.ametys.cms.repository.ContentQueryHelper; 039import org.ametys.cms.repository.ContentTypeExpression; 040import org.ametys.cms.repository.ModifiableDefaultContent; 041import org.ametys.cms.repository.WorkflowAwareContent; 042import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 043import org.ametys.cms.workflow.ContentWorkflowHelper; 044import org.ametys.cms.workflow.CreateContentFunction; 045import org.ametys.odf.course.Course; 046import org.ametys.odf.course.CourseFactory; 047import org.ametys.odf.coursepart.CoursePart; 048import org.ametys.odf.coursepart.CoursePartFactory; 049import org.ametys.odf.enumeration.OdfReferenceTableEntry; 050import org.ametys.odf.enumeration.OdfReferenceTableHelper; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.repository.AmetysRepositoryException; 054import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 055import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 056import org.ametys.plugins.repository.query.expression.AndExpression; 057import org.ametys.plugins.repository.query.expression.Expression; 058import org.ametys.plugins.repository.query.expression.Expression.Operator; 059import org.ametys.plugins.repository.query.expression.MetadataExpression; 060import org.ametys.plugins.repository.query.expression.OrExpression; 061import org.ametys.plugins.workflow.AbstractWorkflowComponent; 062import org.ametys.plugins.workflow.support.WorkflowProvider; 063import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 064import org.ametys.runtime.plugin.component.AbstractLogEnabled; 065 066import com.opensymphony.workflow.InvalidActionException; 067import com.opensymphony.workflow.WorkflowException; 068 069/** 070 * Component class to migrate totalDurationOf* metadata. 071 */ 072public class MigrateCoursePartComponent extends AbstractLogEnabled implements Serviceable, Component 073{ 074 /** The component role. */ 075 public static final String ROLE = MigrateCoursePartComponent.class.getName(); 076 077 /** The Ametys object resolver */ 078 protected AmetysObjectResolver _resolver; 079 /** The ODF Reference table helper */ 080 protected OdfReferenceTableHelper _odfRefTableHelper; 081 /** The content workflow helper */ 082 protected ContentWorkflowHelper _workflowHelper; 083 /** The workflow */ 084 protected WorkflowProvider _workflowProvider; 085 086 @Override 087 public void service(ServiceManager manager) throws ServiceException 088 { 089 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 090 _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 091 _workflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 092 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 093 } 094 095 /** 096 * Migrate the totalDurationOf to course parts. 097 * @throws AmetysRepositoryException if an error occurs 098 * @throws WorkflowException if an error occurs 099 * @throws RepositoryException if an error occurs 100 */ 101 public void migrateCourseParts() throws AmetysRepositoryException, WorkflowException, RepositoryException 102 { 103 migrateCourseParts(null, null); 104 } 105 106 /** 107 * Migrate the totalDurationOf to course parts. 108 * @param natureEnseignementCategories the map of nature enseignement categories 109 * @param natureEnseignements the map of nature enseignement 110 * @throws AmetysRepositoryException if an error occurs 111 * @throws WorkflowException if an error occurs 112 * @throws RepositoryException if an error occurs 113 */ 114 public void migrateCourseParts(Map<String, Pair<String, Long>> natureEnseignementCategories, Map<String, Pair<String, String>> natureEnseignements) throws AmetysRepositoryException, WorkflowException, RepositoryException 115 { 116 Map<String, OdfReferenceTableEntry> natureByCode = _createNaturesEnseignement(natureEnseignementCategories, natureEnseignements); 117 118 // Migrate existing totalDurationOf* 119 Expression[] metadataExpressions = natureByCode.keySet() 120 .stream() 121 .map(code -> new MetadataExpression("totalDurationOf" + code)) 122 .toArray(MetadataExpression[]::new); 123 124 String xpathQuery = ContentQueryHelper.getContentXPathQuery( 125 new AndExpression( 126 new ContentTypeExpression(Operator.EQ, CourseFactory.COURSE_CONTENT_TYPE), 127 new OrExpression(metadataExpressions) 128 )); 129 130 AmetysObjectIterable<Course> courses = _resolver.query(xpathQuery); 131 for (Course course : courses) 132 { 133 boolean hasLiveVersion = ArrayUtils.contains(course.getAllLabels(), CmsConstants.LIVE_LABEL); 134 boolean currentVersionIsLive = hasLiveVersion && ArrayUtils.contains(course.getLabels(), CmsConstants.LIVE_LABEL); 135 136 if (hasLiveVersion && !currentVersionIsLive) 137 { 138 String currentVersion = course.getNode() 139 .getSession() 140 .getWorkspace() 141 .getVersionManager() 142 .getBaseVersion(course.getNode().getPath()) 143 .getName(); 144 145 // switching to old live version 146 course.restoreFromLabel(CmsConstants.LIVE_LABEL); 147 148 // migrate old live version 149 Map<String, CoursePart> createdCourseParts = _migrateCoursePart(course, natureByCode, true); 150 151 // restore current version 152 course.restoreFromRevision(currentVersion); 153 154 // update current version 155 _updateCoursePart(course, natureByCode, createdCourseParts); 156 } 157 else 158 { 159 // migrate current version 160 _migrateCoursePart(course, natureByCode, currentVersionIsLive); 161 } 162 } 163 } 164 165 /** 166 * Update the {@link CoursePart} and the course parts list of the {@link Course} with the values of the current version. 167 * @param course The {@link Course} to migrate 168 * @param natureByCode The course part natures 169 * @param createdCourseParts The {@link Map} of the created {@link CoursePart} previously 170 */ 171 protected void _updateCoursePart(Course course, Map<String, OdfReferenceTableEntry> natureByCode, Map<String, CoursePart> createdCourseParts) 172 { 173 JCRRepositoryData courseRepoData = new JCRRepositoryData(course.getNode()); 174 175 Set<String> courseParts = new HashSet<>(); 176 177 for (String natureCode : natureByCode.keySet()) 178 { 179 String dataName = "totalDurationOf" + natureCode; 180 double totalDurationOf = courseRepoData.hasValue(dataName) ? courseRepoData.getDouble(dataName) : 0; 181 182 if (totalDurationOf > 0) 183 { 184 CoursePart coursePart = createdCourseParts.get(natureCode); 185 if (coursePart == null) 186 { 187 // Create course part 188 Optional.ofNullable(_createCoursePart(course, natureByCode.get(natureCode), totalDurationOf, false)) 189 .ifPresent(createdCoursePart -> courseParts.add(createdCoursePart.getId())); 190 } 191 else 192 { 193 // Update course part 194 double oldValue = coursePart.getValue(CoursePart.NB_HOURS, false, 0D); 195 if (oldValue != totalDurationOf) 196 { 197 coursePart.setValue(CoursePart.NB_HOURS, totalDurationOf); 198 coursePart.saveChanges(); 199 coursePart.checkpoint(); 200 } 201 202 courseParts.add(coursePart.getId()); 203 } 204 } 205 206 removeExternalizableData(courseRepoData, dataName); 207 } 208 209 // Add the course part to the course 210 if (!courseParts.isEmpty()) 211 { 212 courseParts.addAll(ContentDataHelper.getContentIdsListFromMultipleContentData(course, Course.CHILD_COURSE_PARTS)); 213 course.setValue(Course.CHILD_COURSE_PARTS, courseParts.toArray(new String[courseParts.size()])); 214 } 215 else 216 { 217 course.removeExternalizableMetadataIfExists(Course.CHILD_COURSE_PARTS); 218 } 219 220 if (course.needsSave()) 221 { 222 course.saveChanges(); 223 course.checkpoint(); 224 } 225 } 226 227 /** 228 * Create a course part linked to the course. 229 * @param course The {@link Course} to migrate 230 * @param natureByCode The course part natures 231 * @param isLive Set the Live label if <code>true</code> 232 * @return The created course parts 233 */ 234 protected Map<String, CoursePart> _migrateCoursePart(Course course, Map<String, OdfReferenceTableEntry> natureByCode, boolean isLive) 235 { 236 JCRRepositoryData courseRepoData = new JCRRepositoryData(course.getNode()); 237 238 Map<String, CoursePart> createdCourseParts = new HashMap<>(); 239 240 // Create the course parts 241 for (String natureCode : natureByCode.keySet()) 242 { 243 String dataName = "totalDurationOf" + natureCode; 244 double totalDurationOf = courseRepoData.hasValue(dataName) ? courseRepoData.getDouble(dataName) : 0; 245 246 if (totalDurationOf > 0) 247 { 248 Optional.ofNullable(_createCoursePart(course, natureByCode.get(natureCode), totalDurationOf, isLive)) 249 .ifPresent(coursePart -> createdCourseParts.put(natureCode, coursePart)); 250 } 251 252 removeExternalizableData(courseRepoData, dataName); 253 } 254 255 // Add the course part to the course 256 if (!createdCourseParts.isEmpty()) 257 { 258 Set<String> coursePartIds = createdCourseParts.values() 259 .stream() 260 .map(CoursePart::getId) 261 .collect(Collectors.toSet()); 262 263 coursePartIds.addAll(ContentDataHelper.getContentIdsListFromMultipleContentData(course, Course.CHILD_COURSE_PARTS)); 264 course.setValue(Course.CHILD_COURSE_PARTS, coursePartIds.toArray(new String[coursePartIds.size()])); 265 } 266 267 if (course.needsSave()) 268 { 269 // Versions précédentes incompatibles 270 course.addLabel("NotCompatible", true); 271 272 // Sauvegarde et avancement 273 course.saveChanges(); 274 course.checkpoint(); 275 } 276 277 if (isLive) 278 { 279 course.addLabel(CmsConstants.LIVE_LABEL, true); 280 } 281 282 return createdCourseParts; 283 } 284 285 /** 286 * Remove a data and its externalizable data 287 * @param repositoryData the repository data containing the data 288 * @param dataName name of the data to remove 289 */ 290 protected void removeExternalizableData(JCRRepositoryData repositoryData, String dataName) 291 { 292 if (repositoryData.hasValue(dataName)) 293 { 294 repositoryData.removeValue(dataName); 295 } 296 297 if (repositoryData.hasValue(dataName + ModelAwareDataHolder.ALTERNATIVE_SUFFIX)) 298 { 299 repositoryData.removeValue(dataName + ModelAwareDataHolder.ALTERNATIVE_SUFFIX); 300 } 301 302 if (repositoryData.hasValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX)) 303 { 304 repositoryData.removeValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX); 305 } 306 } 307 308 /** 309 * Create a course part linked to the course. 310 * @param course The {@link Course} holder 311 * @param nature The nature of the course part 312 * @param totalDurationOf The number of hours 313 * @param isLive Set the Live label if <code>true</code> 314 * @return The {@link CoursePart} id 315 */ 316 protected CoursePart _createCoursePart(Course course, OdfReferenceTableEntry nature, Double totalDurationOf, boolean isLive) 317 { 318 // Create the course part 319 String coursePartTitle = course.getTitle(); 320 if (nature != null) 321 { 322 coursePartTitle += " - " + nature.getCode(); 323 } 324 String coursePartName = FilterNameHelper.filterName(coursePartTitle); 325 326 Map<String, Object> resultMap = new HashMap<>(); 327 328 Map<String, Object> inputs = new HashMap<>(); 329 inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, course.getLanguage()); 330 inputs.put(CreateContentFunction.CONTENT_NAME_KEY, coursePartName); 331 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, coursePartTitle); 332 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE}); 333 334 Map<String, Object> results = new HashMap<>(); 335 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 336 337 CoursePart coursePart = null; 338 try 339 { 340 Map<String, Object> workflowResult = _workflowHelper.createContent("course-part", 1, coursePartName, coursePartTitle, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE}, null, course.getLanguage()); 341 coursePart = (CoursePart) workflowResult.get(AbstractContentWorkflowComponent.CONTENT_KEY); 342 343 if (nature != null) 344 { 345 coursePart.setValue(CoursePart.NATURE, nature.getId()); 346 } 347 coursePart.setValue(CoursePart.NB_HOURS, totalDurationOf); 348 coursePart.setValue(CoursePart.COURSE_HOLDER, course.getId()); 349 coursePart.setValue(CoursePart.PARENT_COURSES, new String[] {course.getId()}); 350 coursePart.setValue(CoursePart.CATALOG, course.getCatalog()); 351 352 _setAdditionalValues(coursePart, course); 353 354 coursePart.saveChanges(); 355 coursePart.checkpoint(); 356 357 if (isLive) 358 { 359 coursePart.addLabel(CmsConstants.LIVE_LABEL, true); 360 } 361 } 362 catch (WorkflowException e) 363 { 364 resultMap.put("error", Boolean.TRUE); 365 getLogger().error("Failed to initialize workflow for content '{}' and language '{}'", coursePartTitle, course.getLanguage(), e); 366 } 367 368 return coursePart; 369 } 370 371 /** 372 * Set additional values to the {@link CoursePart} from the {@link Course}. 373 * @param coursePart The course part to modify 374 * @param course The original course 375 */ 376 protected void _setAdditionalValues(CoursePart coursePart, Course course) 377 { 378 // Nothing to do 379 } 380 381 /** 382 * Create entries into the reference table EnseignementNature if they don't exist. 383 * @param natureEnseignementCategories the map of nature enseignement categories 384 * @param natureEnseignements the map of nature enseignement 385 * @return The list of natures with their code and associated ID. 386 * @throws AmetysRepositoryException if an error occurs 387 * @throws WorkflowException if an error occurs 388 */ 389 protected Map<String, OdfReferenceTableEntry> _createNaturesEnseignement(Map<String, Pair<String, Long>> natureEnseignementCategories, Map<String, Pair<String, String>> natureEnseignements) throws AmetysRepositoryException, WorkflowException 390 { 391 Map<String, String> categoryByCode = _createNatureEnseignementCategories(natureEnseignementCategories); 392 Map<String, OdfReferenceTableEntry> natureByCode = new HashMap<>(); 393 394 for (Map.Entry<String, Pair<String, String>> nature : _getNaturesEnseignementList(natureEnseignements).entrySet()) 395 { 396 String natureCode = nature.getKey(); 397 OdfReferenceTableEntry natureEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE, natureCode); 398 399 if (natureEntry == null) 400 { 401 String title = nature.getValue().getLeft(); 402 String categoryCode = nature.getValue().getRight(); 403 Map<String, String> titleVariants = new HashMap<>(); 404 titleVariants.put("fr", title); 405 Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, titleVariants, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE}, new String[0]); 406 ModifiableDefaultContent natureContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 407 natureContent.setValue(OdfReferenceTableEntry.CODE, natureCode); 408 natureContent.setValue("category", categoryByCode.get(categoryCode)); 409 natureContent.saveChanges(); 410 _doAction(natureContent, 22); 411 412 natureEntry = new OdfReferenceTableEntry(natureContent); 413 } 414 415 natureByCode.put(natureCode, natureEntry); 416 } 417 418 return natureByCode; 419 } 420 421 /** 422 * Create entries into the reference table EnseignementNature if they don't exist. 423 * @param natureEnseignementCategories the map of nature enseignement categories 424 * @return The list of natures with their code and associated ID. 425 * @throws AmetysRepositoryException if an error occurs 426 * @throws WorkflowException if an error occurs 427 */ 428 protected Map<String, String> _createNatureEnseignementCategories(Map<String, Pair<String, Long>> natureEnseignementCategories) throws AmetysRepositoryException, WorkflowException 429 { 430 Map<String, String> categoryByCode = new HashMap<>(); 431 432 for (Map.Entry<String, Pair<String, Long>> category : _getNaturesEnseignementCategoryList(natureEnseignementCategories).entrySet()) 433 { 434 String categoryCode = category.getKey(); 435 OdfReferenceTableEntry categoryEntry = _getOrCreateNatureEnseignement(category.getValue().getLeft(), categoryCode, category.getValue().getRight()); 436 categoryByCode.put(categoryCode, categoryEntry.getId()); 437 } 438 439 return categoryByCode; 440 } 441 442 /** 443 * List of the course parts natures. 444 * @param natureEnseignements the map of nature enseignement 445 * @return A {@link Map} with the code as a key, and a {@link Pair} with the title and the category code as a value 446 */ 447 protected Map<String, Pair<String, String>> _getNaturesEnseignementList(Map<String, Pair<String, String>> natureEnseignements) 448 { 449 if (natureEnseignements != null && !natureEnseignements.isEmpty()) 450 { 451 return natureEnseignements; 452 } 453 454 Map<String, Pair<String, String>> natures = new HashMap<>(); 455 natures.put("CM", Pair.of("Cours Magistral", "CM")); 456 natures.put("TD", Pair.of("Travaux Dirigés", "TD")); 457 natures.put("TP", Pair.of("Travaux Pratique", "TP")); 458 return natures; 459 } 460 461 /** 462 * List of the course parts nature categories. 463 * @param natureEnseignementCategories the map of nature enseignement categories 464 * @return A {@link Map} with the code as a key, and the title as a value 465 */ 466 protected Map<String, Pair<String, Long>> _getNaturesEnseignementCategoryList(Map<String, Pair<String, Long>> natureEnseignementCategories) 467 { 468 if (natureEnseignementCategories != null && !natureEnseignementCategories.isEmpty()) 469 { 470 return natureEnseignementCategories; 471 } 472 473 Map<String, Pair<String, Long>> categories = new HashMap<>(); 474 categories.put("CM", Pair.of("Cours Magistral", 1L)); 475 categories.put("TD", Pair.of("Travaux Dirigés", 2L)); 476 categories.put("TP", Pair.of("Travaux Pratique", 3L)); 477 return categories; 478 } 479 480 /** 481 * {@link ContentWorkflowHelper} cannot be used in these conditions. 482 * @param content The content 483 * @param actionId Action to perform 484 * @return The result map 485 * @throws WorkflowException if an error occurs 486 * @throws AmetysRepositoryException if an error occurs 487 */ 488 protected Map<String, Object> _doAction(WorkflowAwareContent content, Integer actionId) throws AmetysRepositoryException, WorkflowException 489 { 490 Map<String, Object> inputs = new HashMap<>(); 491 Map<String, Object> results = new HashMap<>(); 492 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 493 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 494 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 495 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, new HashMap<String, Object>()); 496 497 try 498 { 499 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 500 workflow.doAction(content.getWorkflowId(), actionId, inputs); 501 } 502 catch (InvalidActionException e) 503 { 504 getLogger().error("An error occured while do workflow action '{}' on content '{}'", actionId, content.getId(), e); 505 throw e; 506 } 507 508 return results; 509 } 510 511 /** 512 * Get or create the nature enseignement if it doesn't exist. The code is tested. 513 * @param title The title of the nature 514 * @param code The code of the nature 515 * @param order The order to set. 516 * @return The corresponding entry 517 * @throws AmetysRepositoryException if an error occurs 518 * @throws WorkflowException if an error occurs 519 */ 520 protected OdfReferenceTableEntry _getOrCreateNatureEnseignement(String title, String code, Long order) throws AmetysRepositoryException, WorkflowException 521 { 522 OdfReferenceTableEntry categoryEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY, code); 523 524 if (categoryEntry == null) 525 { 526 Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, title, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY}, new String[0], "fr"); 527 ModifiableDefaultContent categoryContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 528 categoryContent.setValue(OdfReferenceTableEntry.CODE, code); 529 categoryContent.setValue("order", order); 530 categoryContent.saveChanges(); 531 _doAction(categoryContent, 22); 532 533 categoryEntry = new OdfReferenceTableEntry(categoryContent); 534 } 535 536 return categoryEntry; 537 } 538}