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