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.data.ContentDataHelper; 037import org.ametys.cms.repository.ContentQueryHelper; 038import org.ametys.cms.repository.ContentTypeExpression; 039import org.ametys.cms.repository.ModifiableDefaultContent; 040import org.ametys.cms.repository.WorkflowAwareContent; 041import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 042import org.ametys.cms.workflow.ContentWorkflowHelper; 043import org.ametys.cms.workflow.CreateContentFunction; 044import org.ametys.odf.course.Course; 045import org.ametys.odf.course.CourseFactory; 046import org.ametys.odf.coursepart.CoursePart; 047import org.ametys.odf.coursepart.CoursePartFactory; 048import org.ametys.odf.enumeration.OdfReferenceTableEntry; 049import org.ametys.odf.enumeration.OdfReferenceTableHelper; 050import org.ametys.plugins.repository.AmetysObjectIterable; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.AmetysRepositoryException; 053import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 054import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 055import org.ametys.plugins.repository.jcr.NameHelper; 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.removeValue(Course.CHILD_COURSE_PARTS); 218 course.removeExternalizableMetadataIfExists(Course.CHILD_COURSE_PARTS); 219 } 220 221 if (course.needsSave()) 222 { 223 course.saveChanges(); 224 course.checkpoint(); 225 } 226 } 227 228 /** 229 * Create a course part linked to the course. 230 * @param course The {@link Course} to migrate 231 * @param natureByCode The course part natures 232 * @param isLive Set the Live label if <code>true</code> 233 * @return The created course parts 234 */ 235 protected Map<String, CoursePart> _migrateCoursePart(Course course, Map<String, OdfReferenceTableEntry> natureByCode, boolean isLive) 236 { 237 JCRRepositoryData courseRepoData = new JCRRepositoryData(course.getNode()); 238 239 Map<String, CoursePart> createdCourseParts = new HashMap<>(); 240 241 // Create the course parts 242 for (String natureCode : natureByCode.keySet()) 243 { 244 String dataName = "totalDurationOf" + natureCode; 245 double totalDurationOf = courseRepoData.hasValue(dataName) ? courseRepoData.getDouble(dataName) : 0; 246 247 if (totalDurationOf > 0) 248 { 249 Optional.ofNullable(_createCoursePart(course, natureByCode.get(natureCode), totalDurationOf, isLive)) 250 .ifPresent(coursePart -> createdCourseParts.put(natureCode, coursePart)); 251 } 252 253 removeExternalizableData(courseRepoData, dataName); 254 } 255 256 // Add the course part to the course 257 if (!createdCourseParts.isEmpty()) 258 { 259 Set<String> coursePartIds = createdCourseParts.values() 260 .stream() 261 .map(CoursePart::getId) 262 .collect(Collectors.toSet()); 263 264 coursePartIds.addAll(ContentDataHelper.getContentIdsListFromMultipleContentData(course, Course.CHILD_COURSE_PARTS)); 265 course.setValue(Course.CHILD_COURSE_PARTS, coursePartIds.toArray(new String[coursePartIds.size()])); 266 } 267 268 if (course.needsSave()) 269 { 270 // Versions précédentes incompatibles 271 course.addLabel("NotCompatible", true); 272 273 // Sauvegarde et avancement 274 course.saveChanges(); 275 course.checkpoint(); 276 } 277 278 if (isLive) 279 { 280 course.addLabel(CmsConstants.LIVE_LABEL, true); 281 } 282 283 return createdCourseParts; 284 } 285 286 /** 287 * Remove a data and its externalizable data 288 * @param repositoryData the repository data containing the data 289 * @param dataName name of the data to remove 290 */ 291 protected void removeExternalizableData(JCRRepositoryData repositoryData, String dataName) 292 { 293 if (repositoryData.hasValue(dataName)) 294 { 295 repositoryData.removeValue(dataName); 296 } 297 298 if (repositoryData.hasValue(dataName + ModelAwareDataHolder.ALTERNATIVE_SUFFIX)) 299 { 300 repositoryData.removeValue(dataName + ModelAwareDataHolder.ALTERNATIVE_SUFFIX); 301 } 302 303 if (repositoryData.hasValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX)) 304 { 305 repositoryData.removeValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX); 306 } 307 } 308 309 /** 310 * Create a course part linked to the course. 311 * @param course The {@link Course} holder 312 * @param nature The nature of the course part 313 * @param totalDurationOf The number of hours 314 * @param isLive Set the Live label if <code>true</code> 315 * @return The {@link CoursePart} id 316 */ 317 protected CoursePart _createCoursePart(Course course, OdfReferenceTableEntry nature, Double totalDurationOf, boolean isLive) 318 { 319 // Create the course part 320 String coursePartTitle = course.getTitle(); 321 if (nature != null) 322 { 323 coursePartTitle += " - " + nature.getCode(); 324 } 325 String coursePartName = NameHelper.filterName(coursePartTitle); 326 327 Map<String, Object> resultMap = new HashMap<>(); 328 329 Map<String, Object> inputs = new HashMap<>(); 330 inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, course.getLanguage()); 331 inputs.put(CreateContentFunction.CONTENT_NAME_KEY, coursePartName); 332 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, coursePartTitle); 333 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE}); 334 335 Map<String, Object> results = new HashMap<>(); 336 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 337 338 CoursePart coursePart = null; 339 try 340 { 341 Map<String, Object> workflowResult = _workflowHelper.createContent("course-part", 1, coursePartName, coursePartTitle, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE}, null, course.getLanguage()); 342 coursePart = (CoursePart) workflowResult.get(AbstractContentWorkflowComponent.CONTENT_KEY); 343 344 if (nature != null) 345 { 346 coursePart.setValue(CoursePart.NATURE, nature.getId()); 347 } 348 coursePart.setValue(CoursePart.NB_HOURS, totalDurationOf); 349 coursePart.setValue(CoursePart.COURSE_HOLDER, course.getId()); 350 coursePart.setValue(CoursePart.PARENT_COURSES, new String[] {course.getId()}); 351 coursePart.setValue(CoursePart.CATALOG, course.getCatalog()); 352 353 _setAdditionalValues(coursePart, course); 354 355 coursePart.saveChanges(); 356 coursePart.checkpoint(); 357 358 if (isLive) 359 { 360 coursePart.addLabel(CmsConstants.LIVE_LABEL, true); 361 } 362 } 363 catch (WorkflowException e) 364 { 365 resultMap.put("error", Boolean.TRUE); 366 getLogger().error("Failed to initialize workflow for content '{}' and language '{}'", coursePartTitle, course.getLanguage(), e); 367 } 368 369 return coursePart; 370 } 371 372 /** 373 * Set additional values to the {@link CoursePart} from the {@link Course}. 374 * @param coursePart The course part to modify 375 * @param course The original course 376 */ 377 protected void _setAdditionalValues(CoursePart coursePart, Course course) 378 { 379 // Nothing to do 380 } 381 382 /** 383 * Create entries into the reference table EnseignementNature if they don't exist. 384 * @param natureEnseignementCategories the map of nature enseignement categories 385 * @param natureEnseignements the map of nature enseignement 386 * @return The list of natures with their code and associated ID. 387 * @throws AmetysRepositoryException if an error occurs 388 * @throws WorkflowException if an error occurs 389 */ 390 protected Map<String, OdfReferenceTableEntry> _createNaturesEnseignement(Map<String, Pair<String, Long>> natureEnseignementCategories, Map<String, Pair<String, String>> natureEnseignements) throws AmetysRepositoryException, WorkflowException 391 { 392 Map<String, String> categoryByCode = _createNatureEnseignementCategories(natureEnseignementCategories); 393 Map<String, OdfReferenceTableEntry> natureByCode = new HashMap<>(); 394 395 for (Map.Entry<String, Pair<String, String>> nature : _getNaturesEnseignementList(natureEnseignements).entrySet()) 396 { 397 String natureCode = nature.getKey(); 398 OdfReferenceTableEntry natureEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE, natureCode); 399 400 if (natureEntry == null) 401 { 402 String title = nature.getValue().getLeft(); 403 String categoryCode = nature.getValue().getRight(); 404 Map<String, String> titleVariants = new HashMap<>(); 405 titleVariants.put("fr", title); 406 Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, titleVariants, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE}, new String[0]); 407 ModifiableDefaultContent natureContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 408 natureContent.setValue(OdfReferenceTableEntry.CODE, natureCode); 409 natureContent.setValue("category", categoryByCode.get(categoryCode)); 410 natureContent.saveChanges(); 411 _doAction(natureContent, 22); 412 413 natureEntry = new OdfReferenceTableEntry(natureContent); 414 } 415 416 natureByCode.put(natureCode, natureEntry); 417 } 418 419 return natureByCode; 420 } 421 422 /** 423 * Create entries into the reference table EnseignementNature if they don't exist. 424 * @param natureEnseignementCategories the map of nature enseignement categories 425 * @return The list of natures with their code and associated ID. 426 * @throws AmetysRepositoryException if an error occurs 427 * @throws WorkflowException if an error occurs 428 */ 429 protected Map<String, String> _createNatureEnseignementCategories(Map<String, Pair<String, Long>> natureEnseignementCategories) throws AmetysRepositoryException, WorkflowException 430 { 431 Map<String, String> categoryByCode = new HashMap<>(); 432 433 for (Map.Entry<String, Pair<String, Long>> category : _getNaturesEnseignementCategoryList(natureEnseignementCategories).entrySet()) 434 { 435 String categoryCode = category.getKey(); 436 OdfReferenceTableEntry categoryEntry = _getOrCreateNatureEnseignement(category.getValue().getLeft(), categoryCode, category.getValue().getRight()); 437 categoryByCode.put(categoryCode, categoryEntry.getId()); 438 } 439 440 return categoryByCode; 441 } 442 443 /** 444 * List of the course parts natures. 445 * @param natureEnseignements the map of nature enseignement 446 * @return A {@link Map} with the code as a key, and a {@link Pair} with the title and the category code as a value 447 */ 448 protected Map<String, Pair<String, String>> _getNaturesEnseignementList(Map<String, Pair<String, String>> natureEnseignements) 449 { 450 if (natureEnseignements != null && !natureEnseignements.isEmpty()) 451 { 452 return natureEnseignements; 453 } 454 455 Map<String, Pair<String, String>> natures = new HashMap<>(); 456 natures.put("CM", Pair.of("Cours Magistral", "CM")); 457 natures.put("TD", Pair.of("Travaux Dirigés", "TD")); 458 natures.put("TP", Pair.of("Travaux Pratique", "TP")); 459 return natures; 460 } 461 462 /** 463 * List of the course parts nature categories. 464 * @param natureEnseignementCategories the map of nature enseignement categories 465 * @return A {@link Map} with the code as a key, and the title as a value 466 */ 467 protected Map<String, Pair<String, Long>> _getNaturesEnseignementCategoryList(Map<String, Pair<String, Long>> natureEnseignementCategories) 468 { 469 if (natureEnseignementCategories != null && !natureEnseignementCategories.isEmpty()) 470 { 471 return natureEnseignementCategories; 472 } 473 474 Map<String, Pair<String, Long>> categories = new HashMap<>(); 475 categories.put("CM", Pair.of("Cours Magistral", 1L)); 476 categories.put("TD", Pair.of("Travaux Dirigés", 2L)); 477 categories.put("TP", Pair.of("Travaux Pratique", 3L)); 478 return categories; 479 } 480 481 /** 482 * {@link ContentWorkflowHelper} cannot be used in these conditions. 483 * @param content The content 484 * @param actionId Action to perform 485 * @return The result map 486 * @throws WorkflowException if an error occurs 487 * @throws AmetysRepositoryException if an error occurs 488 */ 489 protected Map<String, Object> _doAction(WorkflowAwareContent content, Integer actionId) throws AmetysRepositoryException, WorkflowException 490 { 491 Map<String, Object> inputs = new HashMap<>(); 492 Map<String, Object> results = new HashMap<>(); 493 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 494 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 495 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>()); 496 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, new HashMap<>()); 497 498 try 499 { 500 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 501 workflow.doAction(content.getWorkflowId(), actionId, inputs); 502 } 503 catch (InvalidActionException e) 504 { 505 getLogger().error("An error occured while do workflow action '{}' on content '{}'", actionId, content.getId(), e); 506 throw e; 507 } 508 509 return results; 510 } 511 512 /** 513 * Get or create the nature enseignement if it doesn't exist. The code is tested. 514 * @param title The title of the nature 515 * @param code The code of the nature 516 * @param order The order to set. 517 * @return The corresponding entry 518 * @throws AmetysRepositoryException if an error occurs 519 * @throws WorkflowException if an error occurs 520 */ 521 protected OdfReferenceTableEntry _getOrCreateNatureEnseignement(String title, String code, Long order) throws AmetysRepositoryException, WorkflowException 522 { 523 OdfReferenceTableEntry categoryEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY, code); 524 525 if (categoryEntry == null) 526 { 527 Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, title, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY}, new String[0], "fr"); 528 ModifiableDefaultContent categoryContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 529 categoryContent.setValue(OdfReferenceTableEntry.CODE, code); 530 categoryContent.setValue("order", order); 531 categoryContent.saveChanges(); 532 _doAction(categoryContent, 22); 533 534 categoryEntry = new OdfReferenceTableEntry(categoryContent); 535 } 536 537 return categoryEntry; 538 } 539}