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.plugins.odfsync.cdmfr.components; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.Set; 029 030import org.apache.avalon.framework.activity.Initializable; 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.configuration.Configurable; 033import org.apache.avalon.framework.configuration.Configuration; 034import org.apache.avalon.framework.configuration.ConfigurationException; 035import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 036import org.apache.avalon.framework.context.ContextException; 037import org.apache.avalon.framework.context.Contextualizable; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.Constants; 042import org.apache.cocoon.ProcessingException; 043import org.apache.cocoon.environment.Context; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.excalibur.xml.dom.DOMParser; 046import org.apache.excalibur.xml.xpath.XPathProcessor; 047import org.slf4j.Logger; 048import org.w3c.dom.Document; 049import org.w3c.dom.Element; 050import org.w3c.dom.Node; 051import org.w3c.dom.NodeList; 052import org.xml.sax.InputSource; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.ObservationConstants; 056import org.ametys.cms.contenttype.ContentType; 057import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 058import org.ametys.cms.data.ContentSynchronizationResult; 059import org.ametys.cms.repository.ContentQueryHelper; 060import org.ametys.cms.repository.ContentTypeExpression; 061import org.ametys.cms.repository.LanguageExpression; 062import org.ametys.cms.repository.ModifiableContent; 063import org.ametys.cms.repository.WorkflowAwareContent; 064import org.ametys.cms.workflow.ContentWorkflowHelper; 065import org.ametys.cms.workflow.EditContentFunction; 066import org.ametys.core.observation.Event; 067import org.ametys.core.observation.ObservationManager; 068import org.ametys.core.user.CurrentUserProvider; 069import org.ametys.core.user.population.UserPopulationDAO; 070import org.ametys.odf.ODFHelper; 071import org.ametys.odf.ProgramItem; 072import org.ametys.odf.catalog.CatalogsManager; 073import org.ametys.odf.course.Course; 074import org.ametys.odf.course.ShareableCourseHelper; 075import org.ametys.odf.courselist.CourseList; 076import org.ametys.odf.coursepart.CoursePart; 077import org.ametys.odf.enumeration.OdfReferenceTableHelper; 078import org.ametys.odf.observation.OdfObservationConstants; 079import org.ametys.odf.orgunit.OrgUnit; 080import org.ametys.odf.orgunit.RootOrgUnitProvider; 081import org.ametys.odf.program.Program; 082import org.ametys.odf.translation.TranslationHelper; 083import org.ametys.odf.workflow.AbstractCreateODFContentFunction; 084import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 085import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDataProvider; 086import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionHelper; 087import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction; 088import org.ametys.plugins.odfsync.cdmfr.CDMFrSyncExtensionPoint; 089import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractor; 090import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractorFactory; 091import org.ametys.plugins.odfsync.cdmfr.transformers.CDMFrSyncTransformer; 092import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription; 093import org.ametys.plugins.repository.AmetysObjectIterable; 094import org.ametys.plugins.repository.AmetysObjectResolver; 095import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 096import org.ametys.plugins.repository.data.extractor.ModelAwareValuesExtractor; 097import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 098import org.ametys.plugins.repository.jcr.NameHelper; 099import org.ametys.plugins.repository.lock.LockAwareAmetysObject; 100import org.ametys.plugins.repository.query.expression.AndExpression; 101import org.ametys.plugins.repository.query.expression.Expression; 102import org.ametys.plugins.repository.query.expression.Expression.Operator; 103import org.ametys.plugins.repository.query.expression.OrExpression; 104import org.ametys.plugins.repository.query.expression.StringExpression; 105import org.ametys.plugins.workflow.AbstractWorkflowComponent; 106import org.ametys.plugins.workflow.AbstractWorkflowComponent.ConditionFailure; 107import org.ametys.plugins.workflow.component.CheckRightsCondition; 108import org.ametys.runtime.config.Config; 109import org.ametys.runtime.model.ModelItem; 110import org.ametys.runtime.model.View; 111 112import com.opensymphony.workflow.InvalidActionException; 113import com.opensymphony.workflow.WorkflowException; 114 115/** 116 * Abstract class of a component to import a CDM-fr input stream. 117 */ 118public abstract class AbstractImportCDMFrComponent implements ImportCDMFrComponent, Serviceable, Initializable, Contextualizable, Configurable, Component 119{ 120 /** Tag to identify a program */ 121 protected static final String _TAG_PROGRAM = "program"; 122 123 /** Tag to identify a subprogram */ 124 protected static final String _TAG_SUBPROGRAM = "subProgram"; 125 126 /** Tag to identify a container */ 127 protected static final String _TAG_CONTAINER = "container"; 128 129 /** Tag to identify a courseList */ 130 protected static final String _TAG_COURSELIST = "coursesReferences"; 131 132 /** Tag to identify a coursePart */ 133 protected static final String _TAG_COURSEPART = "coursePart"; 134 135 /** The synchronize workflow action id */ 136 protected static final int _SYNCHRONIZE_WORKFLOW_ACTION_ID = 800; 137 138 /** The Cocoon context */ 139 protected Context _cocoonContext; 140 141 /** The DOM parser */ 142 protected DOMParser _domParser; 143 144 /** The XPath processor */ 145 protected XPathProcessor _xPathProcessor; 146 147 /** Extension point to transform CDM-fr */ 148 protected CDMFrSyncExtensionPoint _cdmFrSyncExtensionPoint; 149 150 /** Default language configured for ODF */ 151 protected String _odfLang; 152 153 /** The catalog manager */ 154 protected CatalogsManager _catalogsManager; 155 156 /** The ametys object resolver */ 157 protected AmetysObjectResolver _resolver; 158 159 /** The ODF TableRef Helper */ 160 protected OdfReferenceTableHelper _odfRefTableHelper; 161 162 /** The content type extension point */ 163 protected ContentTypeExtensionPoint _contentTypeEP; 164 165 /** The current user provider */ 166 protected CurrentUserProvider _currentUserProvider; 167 168 /** The observation manager */ 169 protected ObservationManager _observationManager; 170 171 /** The root orgunit provider */ 172 protected RootOrgUnitProvider _rootOUProvider; 173 174 /** The ODF Helper */ 175 protected ODFHelper _odfHelper; 176 177 /** The SCC helper */ 178 protected SynchronizableContentsCollectionHelper _sccHelper; 179 180 /** The content workflow helper */ 181 protected ContentWorkflowHelper _contentWorkflowHelper; 182 183 /** The shareable course helper */ 184 protected ShareableCourseHelper _shareableCourseHelper; 185 186 /** the {@link ImportCDMFrValuesExtractor} factory */ 187 protected ImportCDMFrValuesExtractorFactory _valuesExtractorFactory; 188 189 190 /** List of imported contents */ 191 protected Map<String, Integer> _importedContents; 192 193 /** List of synchronized contents */ 194 protected Set<String> _synchronizedContents; 195 196 /** Number of errors encountered */ 197 protected int _nbError; 198 /** The prefix of the contents */ 199 protected String _contentPrefix; 200 /** Synchronized fields by content type */ 201 protected Map<String, Set<String>> _syncFieldsByContentType; 202 203 public void initialize() throws Exception 204 { 205 _odfLang = Config.getInstance().getValue("odf.programs.lang"); 206 } 207 208 @Override 209 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 210 { 211 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 212 } 213 214 public void configure(Configuration configuration) throws ConfigurationException 215 { 216 _parseSynchronizedFields(); 217 } 218 219 @Override 220 public void service(ServiceManager manager) throws ServiceException 221 { 222 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 223 _domParser = (DOMParser) manager.lookup(DOMParser.ROLE); 224 _xPathProcessor = (XPathProcessor) manager.lookup(XPathProcessor.ROLE); 225 _cdmFrSyncExtensionPoint = (CDMFrSyncExtensionPoint) manager.lookup(CDMFrSyncExtensionPoint.ROLE); 226 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 227 _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 228 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 229 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 230 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 231 _rootOUProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 232 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 233 _sccHelper = (SynchronizableContentsCollectionHelper) manager.lookup(SynchronizableContentsCollectionHelper.ROLE); 234 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 235 _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE); 236 _valuesExtractorFactory = (ImportCDMFrValuesExtractorFactory) manager.lookup(ImportCDMFrValuesExtractorFactory.ROLE); 237 } 238 239 @Override 240 public String getIdField() 241 { 242 return "cdmfrSyncCode"; 243 } 244 245 /** 246 * Get the synchronized metadata from the configuration file 247 * @throws ConfigurationException if the configuration is not valid. 248 */ 249 private void _parseSynchronizedFields() throws ConfigurationException 250 { 251 _syncFieldsByContentType = new HashMap<>(); 252 253 File cdmfrMapping = new File(_cocoonContext.getRealPath("/WEB-INF/param/odf-synchro.xml")); 254 try (InputStream is = !cdmfrMapping.isFile() 255 ? getClass().getResourceAsStream("/org/ametys/plugins/odfsync/cdmfr/odf-synchro.xml") 256 : new FileInputStream(cdmfrMapping)) 257 { 258 Configuration cfg = new DefaultConfigurationBuilder().build(is); 259 260 Configuration[] cTypesConf = cfg.getChildren("content-type"); 261 for (Configuration cTypeConf : cTypesConf) 262 { 263 String contentType = cTypeConf.getAttribute("id"); 264 Set<String> syncAttributes = _configureSynchronizedFields(cTypeConf, StringUtils.EMPTY); 265 _syncFieldsByContentType.put(contentType, syncAttributes); 266 } 267 } 268 catch (Exception e) 269 { 270 throw new ConfigurationException("Error while parsing odf-synchro.xml", e); 271 } 272 } 273 274 private Set<String> _configureSynchronizedFields(Configuration configuration, String prefix) throws ConfigurationException 275 { 276 Set<String> syncAttributes = new HashSet<>(); 277 Configuration[] attributesConf = configuration.getChildren("attribute"); 278 279 if (attributesConf.length > 0) 280 { 281 for (Configuration attributeConf : attributesConf) 282 { 283 if (attributeConf.getChildren("attribute").length > 0) 284 { 285 // composite 286 syncAttributes.addAll(_configureSynchronizedFields(attributeConf, prefix + attributeConf.getAttribute("name") + ModelItem.ITEM_PATH_SEPARATOR)); 287 } 288 else 289 { 290 syncAttributes.add(prefix + attributeConf.getAttribute("name")); 291 } 292 } 293 } 294 else if (configuration.getAttribute("name", null) != null) 295 { 296 syncAttributes.add(prefix + configuration.getAttribute("name")); 297 } 298 299 return syncAttributes; 300 } 301 302 @Override 303 @SuppressWarnings("unchecked") 304 public synchronized Map<String, Object> handleInputStream(InputStream input, Map<String, Object> parameters, SynchronizableContentsCollection scc, Logger logger) throws ProcessingException 305 { 306 List<ModifiableContent> importedPrograms = new ArrayList<>(); 307 308 _importedContents = new HashMap<>(); 309 _synchronizedContents = (Set<String>) parameters.getOrDefault("updatedContents", new HashSet<>()); 310 int nbCreatedContents = (int) parameters.getOrDefault("nbCreatedContents", 0); 311 int nbSynchronizedContents = (int) parameters.getOrDefault("nbSynchronizedContents", 0); 312 _nbError = (int) parameters.getOrDefault("nbError", 0); 313 _contentPrefix = (String) parameters.getOrDefault("contentPrefix", "cdmfr-"); 314 additionalParameters(parameters); 315 316 Map<String, Object> resultMap = new HashMap<>(); 317 318 try 319 { 320 Document doc = _domParser.parseDocument(new InputSource(input)); 321 doc = transformDocument(doc, new HashMap<>(), logger); 322 323 if (doc != null) 324 { 325 String defaultLang = _getXPathString(doc, "CDM/@language", _odfLang); 326 327 NodeList nodes = doc.getElementsByTagName(_TAG_PROGRAM); 328 329 for (int i = 0; i < nodes.getLength(); i++) 330 { 331 Element contentElement = (Element) nodes.item(i); 332 String syncCode = _xPathProcessor.evaluateAsString(contentElement, "@CDMid").trim(); 333 String contentLang = _getXPathString(contentElement, "@language", defaultLang); 334 contentLang = StringUtils.substring(contentLang, 0, 2).toLowerCase(); // on keep the language from the locale 335 336 String catalog = getCatalogName(contentElement); 337 338 ImportCDMFrContext context = new ImportCDMFrContext(scc, doc, contentLang, catalog, logger); 339 importedPrograms.add(importOrSynchronizeContent(contentElement, ContentWorkflowDescription.PROGRAM_WF_DESCRIPTION, syncCode, syncCode, context)); 340 } 341 342 // Validate newly imported contents 343 if (validateAfterImport()) 344 { 345 for (String contentId : _importedContents.keySet()) 346 { 347 WorkflowAwareContent content = _resolver.resolveById(contentId); 348 Integer validationActionId = _importedContents.get(contentId); 349 if (validationActionId > 0) 350 { 351 validateContent(content, validationActionId, logger); 352 } 353 } 354 } 355 } 356 } 357 catch (IOException | ProcessingException e) 358 { 359 throw new ProcessingException("An error occured while transforming the stream.", e); 360 } 361 catch (SAXException e) 362 { 363 throw new ProcessingException("An error occured while parsing the stream.", e); 364 } 365 catch (Exception e) 366 { 367 throw new ProcessingException("An error occured while synchronizing values on contents.", e); 368 } 369 370 resultMap.put("importedContents", _importedContents.keySet()); 371 resultMap.put("nbCreatedContents", nbCreatedContents + _importedContents.size()); 372 resultMap.put("updatedContents", _synchronizedContents); 373 resultMap.put("nbSynchronizedContents", nbSynchronizedContents + _synchronizedContents.size()); 374 resultMap.put("nbError", _nbError); 375 resultMap.put("importedPrograms", importedPrograms); 376 377 return resultMap; 378 } 379 380 /** 381 * True to validate the contents after import 382 * @return True to validate the contents after import 383 */ 384 protected abstract boolean validateAfterImport(); 385 386 /** 387 * When returns true, a content created by a previous synchro will be removed if it does not exist anymore during the current synchro. 388 * @return true if a content created by a previous synchro has to be removed if it does not exist anymore during the current synchro. 389 */ 390 protected abstract boolean removalSync(); 391 392 /** 393 * Additional parameters for specific treatments. 394 * @param parameters The parameters map to get 395 */ 396 protected abstract void additionalParameters(Map<String, Object> parameters); 397 398 /** 399 * Transform the document depending of it structure. 400 * @param document Document to transform. 401 * @param parameters Optional parameters for transformation 402 * @param logger The logger 403 * @return The transformed document. 404 * @throws IOException if an error occurs. 405 * @throws SAXException if an error occurs. 406 * @throws ProcessingException if an error occurs. 407 */ 408 protected Document transformDocument(Document document, Map<String, Object> parameters, Logger logger) throws IOException, SAXException, ProcessingException 409 { 410 Optional<CDMFrSyncTransformer> transformer = _cdmFrSyncExtensionPoint.getFirstSupportingExtension(document); 411 412 if (transformer.isEmpty()) 413 { 414 logger.error("Cannot match a CDM-fr transformer to this file structure."); 415 return null; 416 } 417 418 return transformer.get().transform(document, parameters); 419 } 420 421 public String getCatalogName(Element contentElement) 422 { 423 String defaultCatalog = _catalogsManager.getDefaultCatalogName(); 424 425 String contentCatalog = _getXPathString(contentElement, "catalog", defaultCatalog); 426 if (_catalogsManager.getCatalog(contentCatalog) == null) 427 { 428 // Catalog is empty or do not exist, use the default catalog 429 return defaultCatalog; 430 } 431 432 return contentCatalog; 433 } 434 435 public ModifiableContent importOrSynchronizeContent(Element contentElement, ContentWorkflowDescription wfDescription, String title, String syncCode, ImportCDMFrContext context) 436 { 437 ModifiableContent content = _getOrCreateContent(wfDescription, title, syncCode, context); 438 439 if (content != null) 440 { 441 try 442 { 443 _sccHelper.updateLastSynchronizationProperties(content); 444 content.saveChanges(); 445 _synchronizeContent(contentElement, content, wfDescription.getContentType(), syncCode, context); 446 } 447 catch (Exception e) 448 { 449 _nbError++; 450 context.logger().error("Failed to synchronize data for content {} and language {}.", content, context.lang(), e); 451 } 452 } 453 454 return content; 455 } 456 457 /** 458 * Get or create the content from the workflow description, the synchronization code and the import context. 459 * @param wfDescription The workflow description 460 * @param title The title 461 * @param syncCode The synchronization code 462 * @param context The import context 463 * @return the retrieved or created content 464 */ 465 protected ModifiableContent _getOrCreateContent(ContentWorkflowDescription wfDescription, String title, String syncCode, ImportCDMFrContext context) 466 { 467 ModifiableContent receivedContent = getContent(wfDescription.getContentType(), syncCode, context); 468 if (receivedContent != null) 469 { 470 return receivedContent; 471 } 472 473 try 474 { 475 context.logger().info("Creating content '{}' with the content type '{}' for language {}", title, wfDescription.getContentType(), context.lang()); 476 477 ModifiableContent content = _createContent(wfDescription, title, context); 478 if (content != null) 479 { 480 _sccHelper.updateSCCProperty(content, context.scc().getId()); 481 content.setValue(getIdField(), syncCode); 482 content.saveChanges(); 483 _importedContents.put(content.getId(), wfDescription.getValidationActionId()); 484 } 485 486 return content; 487 } 488 catch (WorkflowException e) 489 { 490 context.logger().error("Failed to initialize workflow for content {} and language {}", title, context.lang(), e); 491 _nbError++; 492 return null; 493 } 494 } 495 496 public ModifiableContent getContent(String contentType, String syncCode, ImportCDMFrContext context) 497 { 498 List<Expression> expList = _getExpressionsList(contentType, syncCode, context); 499 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 500 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 501 502 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery); 503 504 if (contents.getSize() > 0) 505 { 506 return contents.iterator().next(); 507 } 508 509 return null; 510 } 511 512 /** 513 * Create the content from the workflow description and the import context. 514 * @param wfDescription The workflow description 515 * @param title The title 516 * @param context The import context 517 * @return the created content 518 * @throws WorkflowException if an error occurs while creating the content 519 */ 520 protected ModifiableContent _createContent(ContentWorkflowDescription wfDescription, String title, ImportCDMFrContext context) throws WorkflowException 521 { 522 String contentName = NameHelper.filterName(_contentPrefix + "-" + title + "-" + context.lang()); 523 524 Map<String, Object> inputs = _getInputsForContentCreation(wfDescription, context); 525 Map<String, Object> result = _contentWorkflowHelper.createContent( 526 wfDescription.getWorkflowName(), 527 wfDescription.getInitialActionId(), 528 contentName, 529 title, 530 new String[] {wfDescription.getContentType()}, 531 null, 532 context.lang(), 533 inputs); 534 535 return _resolver.resolveById((String) result.get("contentId")); 536 } 537 538 /** 539 * Retrieves the inputs to give for content creation 540 * @param wfDescription The workflow description 541 * @param context The import context 542 * @return the inputs to give for content creation 543 */ 544 protected Map<String, Object> _getInputsForContentCreation(ContentWorkflowDescription wfDescription, ImportCDMFrContext context) 545 { 546 Map<String, Object> inputs = new HashMap<>(); 547 548 ContentType contentType = _contentTypeEP.getExtension(wfDescription.getContentType()); 549 if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG)) 550 { 551 inputs.put(AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY, context.catalog()); 552 } 553 554 return inputs; 555 } 556 557 /** 558 * Synchronize content 559 * @param contentElement the DOM content element 560 * @param content The content to synchronize 561 * @param contentTypeId The content type ID 562 * @param syncCode The synchronization code 563 * @param context the import context 564 * @throws Exception if an error occurs while synchronizing the content values 565 */ 566 protected void _synchronizeContent(Element contentElement, ModifiableContent content, String contentTypeId, String syncCode, ImportCDMFrContext context) throws Exception 567 { 568 Logger logger = context.logger(); 569 570 // Avoid a treatment twice or more 571 if (_synchronizedContents.add(content.getId())) 572 { 573 logger.info("Synchronization of the content '{}' with the content type '{}'", content.getTitle(), contentTypeId); 574 575 if (content instanceof LockAwareAmetysObject && ((LockAwareAmetysObject) content).isLocked()) 576 { 577 logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), ((LockAwareAmetysObject) content).getLockOwner()); 578 } 579 else if (content instanceof WorkflowAwareContent) 580 { 581 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 582 ModelAwareValuesExtractor valuesExtractor = _valuesExtractorFactory.getValuesExtractor(contentElement, this, content, contentType, syncCode, context); 583 584 // Extract the values 585 Map<String, Object> values = valuesExtractor.extractValues(); 586 values.putAll(_getAdditionalValuesToSynchronize(content, syncCode, context)); 587 588 // Modify the content with the extracted values 589 boolean create = _importedContents.containsKey(content.getId()); 590 Set<String> notSynchronizedContentIds = _getNotSynchronizedRelatedContentIds(content, syncCode, context); 591 _editContent((WorkflowAwareContent) content, Optional.empty(), values, create, notSynchronizedContentIds, context); 592 593 if (content instanceof OrgUnit) 594 { 595 _setOrgUnitParent((WorkflowAwareContent) content, context); 596 } 597 598 // Create translation links 599 _linkTranslationsIfExist(content, contentTypeId, context); 600 } 601 } 602 } 603 604 /** 605 * Retrieves additional values to synchronize for the content 606 * @param content the content 607 * @param syncCode the content synchronization code 608 * @param context the import context 609 * @return the additional values 610 */ 611 protected Map<String, Object> _getAdditionalValuesToSynchronize(ModifiableContent content, String syncCode, ImportCDMFrContext context) 612 { 613 Map<String, Object> additionalValues = new HashMap<>(); 614 additionalValues.put(getIdField(), syncCode); 615 return additionalValues; 616 } 617 618 /** 619 * Retrieves the ids of the contents related to the given content but that are not part of the synchronization 620 * @param content the content 621 * @param syncCode the content synchronization code 622 * @param context the import context 623 * @return the not synchronized content ids 624 */ 625 protected Set<String> _getNotSynchronizedRelatedContentIds(ModifiableContent content, String syncCode, ImportCDMFrContext context) 626 { 627 return new HashSet<>(); 628 } 629 630 /** 631 * Synchronize the content with given values. 632 * @param content The content to synchronize 633 * @param view the view containing the item to edit 634 * @param values the values 635 * @param create <code>true</code> if content is creating, false if it is updated 636 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 637 * @param context the import context 638 * @throws WorkflowException if an error occurs 639 */ 640 protected void _editContent(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, boolean create, Set<String> notSynchronizedContentIds, ImportCDMFrContext context) throws WorkflowException 641 { 642 SynchronizationContext synchronizationContext = SynchronizationContext.newInstance() 643 .withStatus(ExternalizableDataStatus.EXTERNAL) 644 .withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, context.scc().getId()) 645 .withIncompatibleValuesIgnored(true); 646 647 if (view.map(v -> content.hasDifferences(v, values, synchronizationContext)) 648 .orElseGet(() -> content.hasDifferences(values, synchronizationContext))) 649 { 650 context.logger().info("Some changes were detected for content '{}' and language {}", content.getTitle(), context.lang()); 651 652 Map<String, Object> inputs = new HashMap<>(); 653 inputs.put(EditSynchronizedContentFunction.SCC_KEY, context.scc()); 654 inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, context.logger()); 655 inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds); 656 if (ignoreRights()) 657 { 658 inputs.put(CheckRightsCondition.FORCE, true); 659 } 660 661 Map<String, Object> params = new HashMap<>(); 662 // Remove catalog data, this value is forced at creation and should not be modified 663 values.remove(ProgramItem.CATALOG); 664 params.put(EditContentFunction.VALUES_KEY, values); 665 view.ifPresent(v -> params.put(EditContentFunction.VIEW, v)); 666 params.put(EditContentFunction.QUIT, true); 667 params.put(EditSynchronizedContentFunction.IMPORT, create); 668 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, params); 669 670 _contentWorkflowHelper.doAction(content, _SYNCHRONIZE_WORKFLOW_ACTION_ID, inputs); 671 } 672 else 673 { 674 context.logger().info("No changes detected for content '{}' and language {}", content.getTitle(), context.lang()); 675 } 676 } 677 678 public ContentSynchronizationResult additionalOperations(ModifiableContent content, Map<String, Object> additionalParameters, Logger logger) 679 { 680 ContentSynchronizationResult result = new ContentSynchronizationResult(); 681 682 if (content instanceof Program) 683 { 684 List<ModifiableContent> modifiedContents = _initializeShareableCoursesFields((Program) content); 685 686 result.addModifiedContents(modifiedContents); 687 result.setHasChanged(!modifiedContents.isEmpty()); 688 } 689 690 return result; 691 } 692 693 /** 694 * Initialize shareable fields for the courses under the given {@link ProgramItem} 695 * @param programItem the program item 696 * @return the list of contents that have been modified during the initialization 697 */ 698 protected List<ModifiableContent> _initializeShareableCoursesFields(ProgramItem programItem) 699 { 700 List<ModifiableContent> modifiedContents = new ArrayList<>(); 701 702 List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem); 703 for (ProgramItem child : children) 704 { 705 if (child instanceof Course && programItem instanceof CourseList) 706 { 707 if (_shareableCourseHelper.initializeShareableFields((Course) child, (CourseList) programItem, UserPopulationDAO.SYSTEM_USER_IDENTITY, true)) 708 { 709 modifiedContents.add((Course) child); 710 } 711 } 712 713 modifiedContents.addAll(_initializeShareableCoursesFields(child)); 714 } 715 716 return modifiedContents; 717 } 718 719 /** 720 * Search for translated contents 721 * @param importedContent The imported content 722 * @param contentType The content type 723 * @param context the import context 724 */ 725 protected void _linkTranslationsIfExist(ModifiableContent importedContent, String contentType, ImportCDMFrContext context) 726 { 727 if (importedContent instanceof ProgramItem) 728 { 729 Expression expression = _getTranslationExpression(importedContent, contentType); 730 String xPathQuery = ContentQueryHelper.getContentXPathQuery(expression); 731 732 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery); 733 734 Map<String, String> translations = new HashMap<>(); 735 for (ModifiableContent content : contents) 736 { 737 translations.put(content.getLanguage(), content.getId()); 738 } 739 740 for (ModifiableContent content : contents) 741 { 742 TranslationHelper.setTranslations(content, translations); 743 744 Map<String, Object> eventParams = new HashMap<>(); 745 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 746 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 747 _observationManager.notify(new Event(OdfObservationConstants.ODF_CONTENT_TRANSLATED, _currentUserProvider.getUser(), eventParams)); 748 } 749 } 750 } 751 752 private Expression _getTranslationExpression(ModifiableContent content, String contentType) 753 { 754 List<Expression> expList = new ArrayList<>(); 755 756 if (StringUtils.isNotBlank(contentType)) 757 { 758 expList.add(new ContentTypeExpression(Operator.EQ, contentType)); 759 } 760 761 String catalog = content.getValue(ProgramItem.CATALOG); 762 if (StringUtils.isNotBlank(catalog)) 763 { 764 expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 765 } 766 767 List<Expression> codeExpressionList = new ArrayList<>(); 768 String syncValue = content.getValue(getIdField()); 769 if (StringUtils.isNotBlank(syncValue)) 770 { 771 codeExpressionList.add(new StringExpression(getIdField(), Operator.EQ, syncValue)); 772 } 773 774 String code = content.getValue(ProgramItem.CODE); 775 if (StringUtils.isNotBlank(syncValue)) 776 { 777 codeExpressionList.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code)); 778 } 779 780 if (!codeExpressionList.isEmpty()) 781 { 782 expList.add(new OrExpression(codeExpressionList.toArray(Expression[]::new))); 783 } 784 785 return new AndExpression(expList.toArray(Expression[]::new)); 786 } 787 788 /** 789 * Set the orgUnit parent to rootOrgUnit. 790 * @param orgUnit The orgunit to link 791 * @param context the import context 792 * @throws Exception if an error occurs while synchronizing the content values 793 */ 794 protected void _setOrgUnitParent(WorkflowAwareContent orgUnit, ImportCDMFrContext context) throws Exception 795 { 796 // Set the orgUnit parent (if no parent is set) 797 if (!orgUnit.hasValue(OrgUnit.PARENT_ORGUNIT)) 798 { 799 OrgUnit rootOrgUnit = _rootOUProvider.getRoot(); 800 Map<String, Object> values = new HashMap<>(); 801 values.put(OrgUnit.PARENT_ORGUNIT, rootOrgUnit); 802 _editContent(orgUnit, Optional.empty(), values, false, Set.of(rootOrgUnit.getId()), context); 803 } 804 } 805 806 /** 807 * Validates a content after import 808 * @param content The content to validate 809 * @param validationActionId Validation action ID to use for this content 810 * @param logger The logger 811 */ 812 protected void validateContent(WorkflowAwareContent content, int validationActionId, Logger logger) 813 { 814 Map<String, Object> inputs = new HashMap<>(); 815 if (ignoreRights()) 816 { 817 inputs.put(CheckRightsCondition.FORCE, true); 818 } 819 820 try 821 { 822 _contentWorkflowHelper.doAction(content, validationActionId, inputs); 823 logger.info("The content {} has been validated after import", content); 824 } 825 catch (WorkflowException | InvalidActionException e) 826 { 827 String failuresAsString = _getActionFailuresAsString(inputs); 828 logger.error("The content {} cannot be validated after import{}", content, failuresAsString, e); 829 } 830 } 831 832 private String _getActionFailuresAsString(Map<String, Object> actionInputs) 833 { 834 String failuresAsString = ""; 835 if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY)) 836 { 837 @SuppressWarnings("unchecked") 838 List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.get(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY); 839 if (!failures.isEmpty()) 840 { 841 failuresAsString = ", due to the following error(s):\n" + String.join("\n", failures.stream().map(ConditionFailure::text).toList()); 842 } 843 } 844 845 return failuresAsString; 846 } 847 848 private String _getXPathString(Node metadataNode, String xPath, String defaultValue) 849 { 850 String value = _xPathProcessor.evaluateAsString(metadataNode, xPath); 851 if (StringUtils.isEmpty(value)) 852 { 853 value = defaultValue; 854 } 855 return value; 856 } 857 858 /** 859 * If true, bypass the rights check during the import process 860 * @return True if the rights check are bypassed during the import process 861 */ 862 protected boolean ignoreRights() 863 { 864 return false; 865 } 866 867 /** 868 * Construct the query to retrieve the content. 869 * @param contentTypeId The content type 870 * @param syncCode The synchronization code 871 * @param context the import context 872 * @return The {@link List} of {@link Expression} 873 */ 874 protected List<Expression> _getExpressionsList(String contentTypeId, String syncCode, ImportCDMFrContext context) 875 { 876 List<Expression> expList = new ArrayList<>(); 877 878 if (StringUtils.isNotBlank(contentTypeId)) 879 { 880 expList.add(new ContentTypeExpression(Operator.EQ, contentTypeId)); 881 882 if (StringUtils.isNotBlank(context.catalog())) 883 { 884 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 885 if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG)) 886 { 887 expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, context.catalog())); 888 } 889 } 890 } 891 892 if (StringUtils.isNotBlank(syncCode)) 893 { 894 expList.add(new StringExpression(getIdField(), Operator.EQ, syncCode)); 895 } 896 897 if (StringUtils.isNotBlank(context.lang())) 898 { 899 expList.add(new LanguageExpression(Operator.EQ, context.lang())); 900 } 901 902 return expList; 903 } 904 905 @Override 906 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 907 { 908 if (additionalParameters == null || !additionalParameters.containsKey("contentTypes")) 909 { 910 throw new IllegalArgumentException("Content types shouldn't be null."); 911 } 912 913 @SuppressWarnings("unchecked") 914 List<String> contentTypeIds = (List<String>) additionalParameters.get("contentTypes"); 915 Set<String> allSyncFields = new HashSet<>(); 916 917 for (String contentTypeId : contentTypeIds) 918 { 919 Set<String> syncFields = _syncFieldsByContentType.computeIfAbsent(contentTypeId, k -> new HashSet<>()); 920 allSyncFields.addAll(syncFields); 921 } 922 923 return allSyncFields; 924 } 925}