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