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