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