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.impl; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import javax.jcr.RepositoryException; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.lang3.StringUtils; 034import org.slf4j.Logger; 035import org.w3c.dom.Document; 036import org.w3c.dom.Node; 037import org.w3c.dom.NodeList; 038 039import org.ametys.cms.content.external.ExternalizableMetadataHelper; 040import org.ametys.cms.contenttype.ContentType; 041import org.ametys.cms.contenttype.MetadataDefinition; 042import org.ametys.cms.contenttype.MetadataType; 043import org.ametys.cms.contenttype.RepeaterDefinition; 044import org.ametys.cms.repository.ContentQueryHelper; 045import org.ametys.cms.repository.ContentTypeExpression; 046import org.ametys.cms.repository.LanguageExpression; 047import org.ametys.cms.repository.ModifiableDefaultContent; 048import org.ametys.odf.ProgramItem; 049import org.ametys.odf.enumeration.OdfReferenceTableHelper; 050import org.ametys.odf.helper.DeleteODFContentHelper; 051import org.ametys.odf.helper.DeleteODFContentHelper.DeleteMode; 052import org.ametys.odf.program.AbstractProgram; 053import org.ametys.odf.program.Program; 054import org.ametys.odf.program.ProgramFactory; 055import org.ametys.odf.program.ProgramPart; 056import org.ametys.odf.program.SubProgram; 057import org.ametys.odf.program.SubProgramFactory; 058import org.ametys.odf.program.TraversableProgramPart; 059import org.ametys.plugins.odfsync.cdmfr.MergeMetadataForSharedProgramHelper; 060import org.ametys.plugins.odfsync.cdmfr.RemoteCDMFrSynchronizableContentsCollection; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.plugins.repository.RepositoryConstants; 063import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 064import org.ametys.plugins.repository.query.expression.AndExpression; 065import org.ametys.plugins.repository.query.expression.Expression; 066import org.ametys.plugins.repository.query.expression.Expression.Operator; 067import org.ametys.plugins.repository.query.expression.StringExpression; 068import org.ametys.runtime.config.Config; 069 070import com.google.common.collect.ImmutableList; 071import com.google.common.collect.ImmutableMap; 072 073/** 074 * Component to import a CDM-fr input stream from a remote server with co-accredited mode. 075 */ 076public class CoAccreditedRemoteImportCDMFrComponent extends RemoteImportCDMFrComponent 077{ 078 /** The name of the JCR node holding the shared metadata */ 079 public static final String SHARED_PROGRAMS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":shared-programs"; 080 081 /** The merge metadata helper */ 082 protected MergeMetadataForSharedProgramHelper _mergeMetadataHelper; 083 084 /** The delete ODF content helper */ 085 protected DeleteODFContentHelper _deleteODFContent; 086 087 /** The list of metadata to copy for mention program */ 088 protected Set<String> _mentionMetadataPaths; 089 090 /** The list of metadata to merge */ 091 protected Set<String> _metadataPathsToMerge; 092 093 private Map<String, String> _degree2MentionMap; 094 private ContentType _mentionContentType; 095 private String _mentionId; 096 private String _programToLinkCode; 097 private String _sharedSubProgramType; 098 099 /** 100 * Enum to define the way to detect shared subProgram 101 */ 102 public enum SharedWithType 103 { 104 /** 105 * The main subProgram set the other shared program in the metadata "shared-with" 106 */ 107 WITH_SHARED_METADATA, 108 109 /** 110 * All subProgram with the same title are shared. The first imported subProgram is the main subProgram. 111 */ 112 WITH_SAME_TITLE, 113 114 115 /** 116 * Shared subProgram are not handled 117 */ 118 NONE 119 } 120 121 @Override 122 public void service(ServiceManager manager) throws ServiceException 123 { 124 super.service(manager); 125 _mergeMetadataHelper = (MergeMetadataForSharedProgramHelper) manager.lookup(MergeMetadataForSharedProgramHelper.ROLE); 126 _deleteODFContent = (DeleteODFContentHelper) manager.lookup(DeleteODFContentHelper.ROLE); 127 } 128 129 @Override 130 public void initialize() throws Exception 131 { 132 super.initialize(); 133 134 _degree2MentionMap = new HashMap<>(); 135 _degree2MentionMap.put(Config.getInstance().getValue("odf.programs.degree.license"), OdfReferenceTableHelper.MENTION_LICENCE); 136 _degree2MentionMap.put(Config.getInstance().getValue("odf.programs.degree.licensepro"), OdfReferenceTableHelper.MENTION_LICENCEPRO); 137 _degree2MentionMap.put(Config.getInstance().getValue("odf.programs.degree.master"), OdfReferenceTableHelper.MENTION_MASTER); 138 139 _mentionContentType = _contentTypeEP.getExtension(getProgramWfDescription().getContentType()); 140 } 141 142 @Override 143 public void configure(Configuration configuration) throws ConfigurationException 144 { 145 super.configure(configuration); 146 147 _mentionMetadataPaths = new HashSet<>(); 148 _metadataPathsToMerge = new HashSet<>(); 149 if ("co-accredited".equals(configuration.getName())) 150 { 151 _configureCoAccreditedParams(configuration); 152 } 153 else 154 { 155 _configureCoAccreditedParams(configuration.getChild("co-accredited")); 156 } 157 } 158 159 /** 160 * Configure the co-accredited params 161 * @param configuration the configuration 162 * @throws ConfigurationException if an error occurred 163 */ 164 protected void _configureCoAccreditedParams(Configuration configuration) throws ConfigurationException 165 { 166 Configuration mentionConf = configuration.getChild("mention"); 167 if (mentionConf != null) 168 { 169 Configuration metadatas = mentionConf.getChild("metadata-to-copy"); 170 if (metadatas != null) 171 { 172 for (Configuration metadataConf : metadatas.getChildren()) 173 { 174 String metadataPath = metadataConf.getAttribute("path"); 175 _mentionMetadataPaths.add(metadataPath); 176 } 177 } 178 } 179 180 Configuration sharedWithConf = configuration.getChild("shared-with"); 181 if (sharedWithConf != null) 182 { 183 Configuration metadatas = sharedWithConf.getChild("metadata-to-merge"); 184 if (metadatas != null) 185 { 186 for (Configuration metadataConf : metadatas.getChildren()) 187 { 188 String metadataPath = metadataConf.getAttribute("path"); 189 _metadataPathsToMerge.add(metadataPath); 190 } 191 } 192 } 193 } 194 195 @Override 196 protected void additionalParameters(Map<String, Object> parameters) 197 { 198 _mentionId = null; 199 _sharedSubProgramType = (String) parameters.getOrDefault(RemoteCDMFrSynchronizableContentsCollection.PARAM_SHARED_WITH_TYPE, SharedWithType.NONE.name()); 200 201 super.additionalParameters(parameters); 202 } 203 204 @Override 205 protected ModifiableDefaultContent _importOrSynchronizeContent(Document doc, Node contentNode, ContentWorkflowDescription wfDescription, String title, String lang, String catalog, String syncCode, Logger logger) 206 { 207 ModifiableDefaultContent content = null; 208 ContentWorkflowDescription contentWfDescription = wfDescription; 209 if (contentNode.getLocalName().equals(_TAG_PROGRAM)) 210 { 211 String educationKind = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.EDUCATION_KIND); 212 String mention = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.MENTION); 213 214 // This is a co-accredited program 215 if (StringUtils.isNotBlank(mention) && "parcours".equals(educationKind)) 216 { 217 // Change the workflow description of the program to subprogram 218 contentWfDescription = getSubProgramWfDescription(); 219 220 // Get or create the mention from the program 221 Program mentionProgram = _getOrCreateMention(doc, contentNode, mention, lang, catalog, logger); 222 _mentionId = mentionProgram.getId(); 223 224 // Store the content to link to the mention 225 _programToLinkCode = syncCode; 226 227 // Handle shared subPrograms 228 if (_getSharedWithType() != SharedWithType.NONE) 229 { 230 content = _importOrSynchronizeSharedSubPrograms(doc, mentionProgram, contentNode, contentWfDescription, title, syncCode, lang, catalog, logger); 231 } 232 } 233 } 234 235 if (content == null) 236 { 237 content = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger); 238 } 239 240 if (_mentionId != null && content instanceof SubProgram && content.getMetadataHolder().getString(getIdField()).equals(_programToLinkCode)) 241 { 242 // Mettre les composantes de la mention à jour 243 ModifiableDefaultContent mention = _resolver.resolveById(_mentionId); 244 List<String> orgunits = ((SubProgram) content).getOrgUnits(); 245 for (String orgunit : orgunits) 246 { 247 _synchroComponent.updateRelation(mention.getMetadataHolder(), AbstractProgram.ORG_UNITS_REFERENCES, orgunit, false); 248 } 249 } 250 251 return content; 252 } 253 254 255 /** 256 * Import or synchronized shared subPrograms 257 * @param doc the document 258 * @param mentionProgram the mention program 259 * @param contentNode the content node 260 * @param contentWfDescription the content workflow description 261 * @param title the title of the subprogram 262 * @param syncCode the synchronisation code 263 * @param lang the language 264 * @param catalog the catalog 265 * @param logger the logger 266 * @return the imported of synchronized content 267 */ 268 protected ModifiableDefaultContent _importOrSynchronizeSharedSubPrograms(Document doc, Program mentionProgram, Node contentNode, ContentWorkflowDescription contentWfDescription, String title, String syncCode, String lang, String catalog, Logger logger) 269 { 270 // Is imported program already exist as a subprogram ? 271 ModifiableDefaultContent subProgram = _getContent(lang, catalog, syncCode, contentWfDescription); 272 if (subProgram == null) 273 { 274 // The imported program does not exist as subprogram, create it except if it is a shared subprogram 275 if (_isSecondarySharedSubPrograms(mentionProgram, contentNode, lang, logger)) 276 { 277 // Search if there is a main subprogram shared with it. 278 SubProgram mainSubProgram = getMainSharedSubProgram(mentionProgram, contentNode, lang, logger); 279 if (mainSubProgram != null) 280 { 281 // The imported program is a shared subprogram, it will not be imported nor synchronized, only the main subprogram will be updated 282 try 283 { 284 // Merge shared program metadata from CDMfr 285 if (_synchronizeSharedMetadata(doc, contentNode, mainSubProgram, null, _metadataPathsToMerge, catalog, lang, logger)) 286 { 287 _mergeMetadataHelper.mergeSharedMetadata(mainSubProgram, contentNode, _metadataPathsToMerge, logger); 288 } 289 } 290 catch (RepositoryException e) 291 { 292 String warnMsg = "Impossible de synchronizer les métadonnées partagées pour le parcours principal \"" + mainSubProgram.getTitle() + "\""; 293 logger.warn(warnMsg); 294 } 295 296 return mainSubProgram; 297 } 298 else 299 { 300 return super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger); 301 } 302 } 303 else 304 { 305 subProgram = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger); 306 307 try 308 { 309 // Synchronize the shared metadata of the main program to be merged later 310 _synchronizeSharedMetadata(doc, contentNode, (SubProgram) subProgram, null, _metadataPathsToMerge, catalog, lang, logger); 311 } 312 catch (RepositoryException e) 313 { 314 String warnMsg = "Impossible de synchronizer les métadonnées partagées du parcours principal \"" + subProgram.getTitle() + "\""; 315 logger.warn(warnMsg); 316 } 317 318 // Browse existing shared subprograms to first merge them then delete them. 319 // When we shared subprograms with the title, it can't have other created shared subPrograms because the first one is the main shared subprogram 320 SharedWithType sharedWithType = _getSharedWithType(); 321 if (SharedWithType.WITH_SHARED_METADATA == sharedWithType) 322 { 323 List<String> sharedWith = _getSharedWithAsString(contentNode, logger); 324 _synchronizeAndDeleteSharedSubPrograms(mentionProgram, (SubProgram) subProgram, sharedWith, catalog, lang, logger); 325 326 try 327 { 328 // Then merge main subprogram 329 _mergeMetadataHelper.mergeSharedMetadata((SubProgram) subProgram, contentNode, _metadataPathsToMerge, logger); 330 } 331 catch (RepositoryException e) 332 { 333 String warnMsg = "Impossible de synchronizer les metadatas du programme principal \"" + subProgram.getTitle() + "\""; 334 logger.warn(warnMsg); 335 } 336 } 337 338 return subProgram; 339 } 340 } 341 else // The imported program exists as subprogram 342 { 343 ModifiableDefaultContent mainSubProgram = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger); 344 345 if (!_isSecondarySharedSubPrograms(mentionProgram, contentNode, lang, logger)) 346 { 347 // When we shared subprograms with the title, it can't have other created shared subPrograms because the first one is the main shared subprogram 348 SharedWithType sharedWithType = _getSharedWithType(); 349 if (SharedWithType.WITH_SHARED_METADATA == sharedWithType) 350 { 351 List<String> sharedWith = _getSharedWithAsString(contentNode, logger); 352 353 // Browse existing shared subprograms to first merge them then delete them. 354 _synchronizeAndDeleteSharedSubPrograms(mentionProgram, (SubProgram) subProgram, sharedWith, catalog, lang, logger); 355 } 356 357 try 358 { 359 // Synchronize the shared metadata of the main program to be merged later 360 _synchronizeSharedMetadata(doc, contentNode, (SubProgram) subProgram, null, _metadataPathsToMerge, catalog, lang, logger); 361 // Then merge main subprogram 362 _mergeMetadataHelper.mergeSharedMetadata((SubProgram) subProgram, contentNode, _metadataPathsToMerge, logger); 363 } 364 catch (RepositoryException e) 365 { 366 String warnMsg = "Impossible de synchronizer les metadatas du programme principal \"" + subProgram.getTitle() + "\""; 367 logger.warn(warnMsg); 368 } 369 } 370 371 return mainSubProgram; 372 } 373 } 374 375 /** 376 * Get of create the mention program 377 * @param doc the document 378 * @param contentNode the content node 379 * @param mentionCode the mention code 380 * @param lang the language 381 * @param catalog the catalog 382 * @param logger the logger 383 * @return the mention program 384 */ 385 protected Program _getOrCreateMention(Document doc, Node contentNode, String mentionCode, String lang, String catalog, Logger logger) 386 { 387 String degreeCodeCDM = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.DEGREE); 388 String degreeCode = _odfRefTableHelper.getItemCodeFromCDM(OdfReferenceTableHelper.DEGREE, degreeCodeCDM); 389 if (degreeCode == null) 390 { 391 degreeCode = degreeCodeCDM; 392 } 393 394 String mentionType = _degree2MentionMap.get(degreeCode); 395 if (mentionType != null) 396 { 397 String mentionId = _getIdFromCDMThenCode(mentionType, mentionCode); 398 if (mentionId != null) 399 { 400 String degreeId = _getIdFromCDMThenCode(OdfReferenceTableHelper.DEGREE, degreeCode); 401 Program mention = _getMention(mentionId, degreeId, lang, catalog); 402 if (mention == null) 403 { 404 mention = _createMention(doc, contentNode, mentionId, catalog, lang, logger); 405 _importedContents.put(mention.getId(), getProgramWfDescription().getValidationActionId()); 406 } 407 408 return mention; 409 } 410 else 411 { 412 logger.error("Il n'y a pas de code associé à la mention {}. La formation n'a pas été importée.", mentionCode); 413 _nbError++; 414 } 415 } 416 else 417 { 418 logger.error("Il n'y a pas de type de mention (licence, licence pro, master) associée au diplôme {}. La formation n'a pas été importée.", degreeCode); 419 _nbError++; 420 } 421 422 return null; 423 } 424 425 /** 426 * Create the mention 427 * @param doc the document 428 * @param contentNode the content node 429 * @param mentionId the mention id 430 * @param catalog the catalog 431 * @param lang the language 432 * @param logger the logger 433 * @return the created mention 434 */ 435 protected Program _createMention(Document doc, Node contentNode, String mentionId, String catalog, String lang, Logger logger) 436 { 437 String contentTitle = _odfRefTableHelper.getItemLabel(mentionId, lang); 438 ContentWorkflowDescription wfDescription = getProgramWfDescription(); 439 Map<String, Object> resultMap = _synchroComponent.createContentAction(wfDescription.getContentType(), wfDescription.getWorkflowName(), wfDescription.getInitialActionId(), lang, contentTitle, _contentPrefix, logger); 440 if ((boolean) resultMap.getOrDefault("error", false)) 441 { 442 _nbError++; 443 } 444 445 Program mention = (Program) resultMap.get("content"); 446 447 if (mention != null) 448 { 449 boolean hasChanges = false; 450 if (catalog != null) 451 { 452 hasChanges = ExternalizableMetadataHelper.setMetadata(mention.getMetadataHolder(), ProgramItem.METADATA_CATALOG, catalog); 453 } 454 455 hasChanges = ExternalizableMetadataHelper.setExternalMetadata(mention.getMetadataHolder(), AbstractProgram.MENTION, mentionId, true) || hasChanges; 456 hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, AbstractProgram.DEGREE, lang, catalog, logger) || hasChanges; 457 hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, AbstractProgram.DOMAIN, lang, catalog, logger) || hasChanges; 458 459 for (String metadataPath : _mentionMetadataPaths) 460 { 461 hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, metadataPath, lang, catalog, logger) || hasChanges; 462 } 463 464 _saveContentChanges(mention, wfDescription.getContentType(), hasChanges, logger); 465 } 466 467 return mention; 468 } 469 470 /** 471 * Get the mention program 472 * @param mentionId the mention content id 473 * @param degreeId the degree content id 474 * @param lang the language 475 * @param catalog the catalog 476 * @return the mention program or <code>null</code> if it doesn't exist 477 */ 478 protected Program _getMention(String mentionId, String degreeId, String lang, String catalog) 479 { 480 List<Expression> expList = new ArrayList<>(); 481 expList.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 482 expList.add(new LanguageExpression(Operator.EQ, lang)); 483 expList.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalog)); 484 expList.add(new StringExpression(AbstractProgram.DEGREE, Operator.EQ, degreeId)); 485 expList.add(new StringExpression(AbstractProgram.MENTION, Operator.EQ, mentionId)); 486 487 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 488 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 489 490 AmetysObjectIterable<Program> contents = _resolver.query(xPathQuery); 491 492 if (contents.getSize() > 0) 493 { 494 return contents.iterator().next(); 495 } 496 497 return null; 498 } 499 500 /** 501 * Synchronize metadata in the mention program 502 * @param doc the document 503 * @param contentNode the content node 504 * @param mention the mention program 505 * @param metadataPath the metadata path to synchronize 506 * @param lang the language 507 * @param catalog the catalog 508 * @param logger the logger 509 * @return true if some changes were made 510 */ 511 protected boolean _synchronizeMentionMetadata(Document doc, Node contentNode, ModifiableDefaultContent mention, String metadataPath, String lang, String catalog, Logger logger) 512 { 513 Node metadataNode = _xPathProcessor.selectSingleNode(contentNode, metadataPath); 514 if (metadataNode != null) 515 { 516 return _synchronizeMetadata(doc, metadataNode, mention, metadataPath, metadataPath, _mentionContentType, lang, catalog, logger); 517 } 518 return false; 519 } 520 521 @Override 522 protected void additionalOperationsBeforeSave(ModifiableDefaultContent content, Logger logger) throws RepositoryException 523 { 524 if (_mentionId != null && content instanceof SubProgram && content.getMetadataHolder().getString(getIdField()).equals(_programToLinkCode)) 525 { 526 boolean hasChanges = false; 527 528 ModifiableDefaultContent mentionContent = _resolver.resolveById(_mentionId); 529 530 // Relier le programme à la mention 531 hasChanges = _synchroComponent.updateRelation(mentionContent.getMetadataHolder(), TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, content, false) || hasChanges; 532 if (_synchroComponent.updateRelation(content.getMetadataHolder(), ProgramPart.METADATA_PARENT_PROGRAM_PARTS, _mentionId, false)) 533 { 534 _saveContentChanges(content, getSubProgramWfDescription().getContentType(), true, logger); 535 } 536 537 if (hasChanges) 538 { 539 _saveContentChanges(mentionContent, _mentionContentType.getId(), hasChanges, logger); 540 } 541 } 542 } 543 544 /** 545 * Get the defined way to detect shared program 546 * @return shared with type 547 */ 548 protected SharedWithType _getSharedWithType() 549 { 550 return SharedWithType.valueOf(_sharedSubProgramType); 551 } 552 553 /** 554 * Get the main subprogram shared with the program representing by the content node 555 * @param mentionProgram the root mention program 556 * @param contentNode the content node 557 * @param lang the content lang 558 * @param logger the logger 559 * @return the main subprogram if the program is a shared subprogram or <code>null</code> otherwise 560 */ 561 protected SubProgram getMainSharedSubProgram(Program mentionProgram, Node contentNode, String lang, Logger logger) 562 { 563 SharedWithType sharedWithType = _getSharedWithType(); 564 switch (sharedWithType) 565 { 566 case WITH_SHARED_METADATA: 567 return _getMainSharedWithSubProgram(mentionProgram, contentNode, logger); 568 case WITH_SAME_TITLE: 569 return _getSubProgramWithSameTitle(mentionProgram, contentNode, lang); 570 default: 571 return null; 572 } 573 } 574 575 /** 576 * True if the subProgram node is shared with a main subProgram. 577 * False if the subProgram node is not shared or if the main subProgram is not already imported 578 * @param mentionProgram the mention program 579 * @param contentNode the content node 580 * @param lang the lang 581 * @param logger the logger 582 * @return true if it's a secondary subProgram 583 */ 584 protected boolean _isSecondarySharedSubPrograms(Program mentionProgram, Node contentNode, String lang, Logger logger) 585 { 586 SharedWithType sharedWithType = _getSharedWithType(); 587 switch (sharedWithType) 588 { 589 case WITH_SHARED_METADATA: 590 List<String> sharedWith = _getSharedWithAsString(contentNode, logger); 591 return sharedWith.isEmpty(); 592 case WITH_SAME_TITLE: 593 return _hasSubProgramWithSameTitle(mentionProgram, contentNode, lang, logger); 594 default: 595 return false; 596 } 597 } 598 599 /** 600 * True if there is a subProgram with the same title of the content node 601 * @param mentionProgram the mention program 602 * @param contentNode the content node 603 * @param lang the lang 604 * @param logger the logger 605 * @return true if there is a subProgram with the same title of the content node 606 */ 607 protected boolean _hasSubProgramWithSameTitle(Program mentionProgram, Node contentNode, String lang, Logger logger) 608 { 609 SubProgram subProgram = _getSubProgramWithSameTitle(mentionProgram, contentNode, lang); 610 return subProgram != null; 611 } 612 613 614 /** 615 * Get the subProgram with the same title 616 * @param mentionProgram the mention program 617 * @param contentNode the content node 618 * @param lang the lang 619 * @return the subProgram with the same title. null if there are no subProgram with same title. 620 */ 621 protected SubProgram _getSubProgramWithSameTitle(Program mentionProgram, Node contentNode, String lang) 622 { 623 String contentTitle = _xPathProcessor.evaluateAsString(contentNode, "title"); 624 625 for (ProgramPart child : mentionProgram.getProgramPartChildren()) 626 { 627 if (child instanceof SubProgram) 628 { 629 SubProgram subProgram = (SubProgram) child; 630 if (subProgram.getTitle().equals(contentTitle)) 631 { 632 return subProgram; 633 } 634 } 635 } 636 return null; 637 } 638 639 /** 640 * Get the list of "shared with" program from the remote document 641 * @param contentNode The content node 642 * @param logger the logger 643 * @return The CDM code of shared program 644 */ 645 protected List<String> _getSharedWithAsString (Node contentNode, Logger logger) 646 { 647 List<String> codes = new ArrayList<>(); 648 649 Node sharedWithNode = _xPathProcessor.selectSingleNode(contentNode, AbstractProgram.SHARED_WITH); 650 651 if (sharedWithNode != null) 652 { 653 NodeList itemNodes = sharedWithNode.getChildNodes(); 654 for (int j = 0; j < itemNodes.getLength(); j++) 655 { 656 String code = itemNodes.item(j).getTextContent().trim(); 657 if (StringUtils.isNotBlank(code)) 658 { 659 codes.add(code); 660 } 661 } 662 } 663 664 return codes; 665 } 666 667 /** 668 * Get the main subprogram shared with the program representing by the content node 669 * @param mentionProgram the root mention program 670 * @param contentNode the content node 671 * @param logger the logger 672 * @return the main subprogram if the program is a shared subprogram or <code>null</code> otherwise 673 */ 674 protected SubProgram _getMainSharedWithSubProgram(Program mentionProgram, Node contentNode, Logger logger) 675 { 676 String programCode = _xPathProcessor.evaluateAsString(contentNode, ProgramItem.METADATA_CODE); 677 678 for (ProgramPart child : mentionProgram.getProgramPartChildren()) 679 { 680 if (child instanceof SubProgram) 681 { 682 SubProgram subProgram = (SubProgram) child; 683 List<String> sharedProgram = Arrays.asList(subProgram.getSharedWith()); 684 if (sharedProgram.contains(programCode)) 685 { 686 return subProgram; 687 } 688 } 689 } 690 return null; 691 } 692 693 /** 694 * Get the shared subprogram with the given code 695 * @param mentionProgram The root mention program 696 * @param code The CDMfr code 697 * @param logger the logger 698 * @return The shared subprogram if exist or <code>null</code> if not found 699 */ 700 protected SubProgram _getSharedSubProgram (Program mentionProgram, String code, Logger logger) 701 { 702 for (ProgramPart child : mentionProgram.getProgramPartChildren()) 703 { 704 if (child instanceof SubProgram) 705 { 706 SubProgram subProgram = (SubProgram) child; 707 if (subProgram.getCode().equals(code)) 708 { 709 return subProgram; 710 } 711 } 712 } 713 return null; 714 } 715 716 /** 717 * Delete a shared secondary subprogram. 718 * This subprogram was created by a precede import before the main subprogram was imported 719 * @param sharedProgram the shared program 720 * @param logger the logger 721 */ 722 protected void deleteSharedSubProgram(ModifiableDefaultContent sharedProgram, Logger logger) 723 { 724 String title = sharedProgram.getTitle(); 725 String infoMsg = "Suppression du parcours partagé secondaire \"" + title + "\""; 726 logger.info(infoMsg); 727 728 String contentId = sharedProgram.getId(); 729 List<String> contentIds = Collections.singletonList(contentId); 730 731 // Delete content bypassing the rights check 732 Map<String, Object> results = _deleteODFContent.deleteContentsWithLog(contentIds, DeleteMode.FULL.name(), true); 733 734 @SuppressWarnings("unchecked") 735 Map<String, Object> result = (Map<String, Object>) results.get(contentId); 736 if (result.containsKey("check-before-deletion-failed") && (boolean) result.get("check-before-deletion-failed")) 737 { 738 logger.warn("Can't deleted shared content " + title + " (" + contentId + "). See previous logs for more information."); 739 } 740 } 741 742 /** 743 * Synchronize the shared metadata of a shared subprogram before deleting the subprogram 744 * @param rootProgram The root mention 745 * @param mainSubProgram The main subprogram 746 * @param sharedWith The CDMfr of shared subprogram 747 * @param catalog the catalog 748 * @param lang the language 749 * @param logger The logger 750 * @return <code>true</code> if changes were made 751 */ 752 protected boolean _synchronizeAndDeleteSharedSubPrograms (Program rootProgram, SubProgram mainSubProgram, List<String> sharedWith, String catalog, String lang, Logger logger) 753 { 754 boolean hasChanged = false; 755 756 // Browse existing shared subprograms to first merge them then delete them. 757 for (String code : sharedWith) 758 { 759 SubProgram sharedSubProgram = _getSharedSubProgram(rootProgram, code, logger); 760 if (sharedSubProgram != null) 761 { 762 String infoMsg = "Fusion avec le parcours partagé \"" + sharedSubProgram.getTitle() + "\""; 763 logger.info(infoMsg); 764 765 try 766 { 767 _synchronizeSharedMetadata(null, null, mainSubProgram, sharedSubProgram, _metadataPathsToMerge, catalog, lang, logger); 768 } 769 catch (RepositoryException e) 770 { 771 String warnMsg = "Impossible de synchronizer les metadatas du programme partagé \"" + sharedSubProgram.getTitle() + "\" avec le programme principal \"" + mainSubProgram.getTitle() + "\""; 772 logger.warn(warnMsg); 773 } 774 775 // Then delete the secondary shared subprogram 776 deleteSharedSubProgram(sharedSubProgram, logger); 777 hasChanged = true; 778 } 779 } 780 781 return hasChanged; 782 } 783 784 /** 785 * Synchronize the shared program node for a main subprogram, from a secondary subprogram 786 * @param doc the document. Can be null if the sharedProgram is not 787 * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not 788 * @param mainProgram the main program 789 * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not 790 * @param metadataToMerge the set of metadata to merge 791 * @param catalog the catalog 792 * @param lang the language 793 * @param logger the logger 794 * @return <code>true</code> if changes were made 795 * @throws RepositoryException if an error occurred 796 */ 797 public boolean _synchronizeSharedMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, Set<String> metadataToMerge, String catalog, String lang, Logger logger) throws RepositoryException 798 { 799 boolean hasChanged = false; 800 801 ModifiableCompositeMetadata sharedMetadataHolder = _getSharedMetadataHolder(doc, sharedProgramNode, mainProgram, sharedProgram, logger); 802 for (String metadataPath : metadataToMerge) 803 { 804 ContentType cType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 805 806 if (_mergeMetadataHelper.isValidMetadataPath(cType, metadataPath, logger)) 807 { 808 String[] pathSegments = StringUtils.split(metadataPath, '/'); 809 MetadataDefinition metadataDefinition = cType.getMetadataDefinition(pathSegments[0]); 810 811 if (metadataDefinition.isMultiple()) 812 { 813 hasChanged = _synchronizeSharedMultipleMetadata(doc, sharedProgramNode, mainProgram, sharedProgram, metadataDefinition, sharedMetadataHolder, metadataPath, catalog, lang, logger) || hasChanged; 814 } 815 else 816 { 817 hasChanged = _synchronizeSharedSingleMetadata(doc, sharedProgramNode, mainProgram, sharedProgram, metadataDefinition, sharedMetadataHolder, metadataPath, catalog, lang, logger) || hasChanged; 818 } 819 } 820 } 821 822 return hasChanged; 823 } 824 825 /** 826 * Get the metadata composite holding shared metadata of subprograms 827 * @param doc the document. Can be null if the sharedProgram is not 828 * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not 829 * @param mainProgram the main program 830 * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not 831 * @param logger the logger 832 * @return the shared metadata holder 833 * @throws RepositoryException if an error occurred 834 */ 835 protected ModifiableCompositeMetadata _getSharedMetadataHolder(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, Logger logger) throws RepositoryException 836 { 837 // The data comes from a content 838 if (doc == null) 839 { 840 return _mergeMetadataHelper.getSharedMetadataHolder(mainProgram, sharedProgram.getCode(), logger); 841 } 842 else // The data comes from the DOM 843 { 844 String sharedProgramCode = _xPathProcessor.evaluateAsString(sharedProgramNode, ProgramItem.METADATA_CODE); 845 return _mergeMetadataHelper.getSharedMetadataHolder(mainProgram, sharedProgramCode, logger); 846 } 847 } 848 849 /** 850 * Synchronize a shared multiple metadata for shared program 851 * @param doc the document. Can be null if the sharedProgram is not 852 * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not 853 * @param mainProgram the main program 854 * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not 855 * @param metadataDefinition the metadata definition 856 * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms 857 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 858 * @param catalog the catalog 859 * @param lang the language 860 * @param logger the logger 861 * @return <code>true</code> if changes were made 862 */ 863 protected boolean _synchronizeSharedMultipleMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger) 864 { 865 boolean hasChanged = false; 866 ContentType contentType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 867 Map<String, Object> params = ImmutableMap.of("contentType", contentType.getId()); 868 boolean synchronize = getLocalAndExternalFields(params).contains(logicalMetadataPath); 869 870 MetadataType type = metadataDefinition.getType(); 871 872 // Data comes from a content 873 List<Object> metadataValues = new ArrayList<>(); 874 if (doc == null) 875 { 876 metadataValues = _getMultipleValuesFromContent(sharedProgram, logicalMetadataPath, type, logger); 877 } 878 else 879 { 880 metadataValues = _getMultipleValuesFromDOM(doc, sharedProgramNode, mainProgram, metadataDefinition, logicalMetadataPath, type, catalog, lang, logger); 881 } 882 883 884 Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(mainProgram, contentType, logicalMetadataPath, sharedMetadataHolder, logicalMetadataPath, metadataValues, synchronize, false, logger); 885 if (resultMap.getOrDefault("error", Boolean.FALSE)) 886 { 887 _nbError++; 888 } 889 hasChanged = resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue(); 890 891 return hasChanged; 892 } 893 894 /** 895 * Get multiple values from content 896 * @param sharedProgram the shared program 897 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 898 * @param type the type of the data to retrieve 899 * @param logger the logger 900 * @return the list of values 901 */ 902 protected List<Object> _getMultipleValuesFromContent(SubProgram sharedProgram, String logicalMetadataPath, MetadataType type, Logger logger) 903 { 904 List<Object> metadataValues = new ArrayList<>(); 905 ModifiableCompositeMetadata metadataHolder = sharedProgram.getMetadataHolder(); 906 switch (type) 907 { 908 case STRING: 909 case REFERENCE: 910 case CONTENT: 911 for (String value : metadataHolder.getStringArray(logicalMetadataPath, new String[0])) 912 { 913 metadataValues.add(value); 914 } 915 break; 916 case DOUBLE: 917 for (double value : metadataHolder.getDoubleArray(logicalMetadataPath, new double[0])) 918 { 919 metadataValues.add(value); 920 } 921 break; 922 case LONG: 923 for (long value : metadataHolder.getLongArray(logicalMetadataPath, new long[0])) 924 { 925 metadataValues.add(value); 926 } 927 break; 928 default: 929 String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée."; 930 logger.warn(warn); 931 break; 932 } 933 934 return metadataValues; 935 } 936 937 /** 938 * Get multiple values from DOM 939 * @param doc the document 940 * @param sharedProgramDOM the shared program node 941 * @param mainProgram the main program 942 * @param metadataDefinition the metadata definition 943 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 944 * @param type the type of the data to retrieve 945 * @param catalog the catalog 946 * @param lang the lang 947 * @param logger the logger 948 * @return the list of values 949 */ 950 protected List<Object> _getMultipleValuesFromDOM (Document doc, Node sharedProgramDOM, SubProgram mainProgram, MetadataDefinition metadataDefinition, String logicalMetadataPath, MetadataType type, String catalog, String lang, Logger logger) 951 { 952 List<String> metadataValues = new ArrayList<>(); 953 954 Node metadataNode = _xPathProcessor.selectSingleNode(sharedProgramDOM, logicalMetadataPath); 955 if (metadataNode != null) 956 { 957 switch (type) 958 { 959 case STRING: 960 case REFERENCE: 961 case CONTENT: 962 NodeList itemNodes = metadataNode.getChildNodes(); 963 for (int j = 0; j < itemNodes.getLength(); j++) 964 { 965 String metadataValue = itemNodes.item(j).getTextContent().trim(); 966 if (StringUtils.isNotEmpty(metadataValue)) 967 { 968 metadataValues.add(metadataValue); 969 } 970 } 971 break; 972 default: 973 String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée."; 974 logger.warn(warn); 975 } 976 } 977 978 return _handleMetadataValues(doc, mainProgram, metadataDefinition, metadataValues, lang, catalog, logger); 979 } 980 981 /** 982 * Synchronize single metadata for shared program 983 * @param doc the document. Can be null if the sharedProgram is not 984 * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not 985 * @param mainProgram the main program 986 * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not 987 * @param metadataDefinition the metadata definition 988 * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms 989 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 990 * @param catalog the catalog 991 * @param lang the language 992 * @param logger the logger 993 * @return <code>true</code> if changes were made 994 */ 995 protected boolean _synchronizeSharedSingleMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger) 996 { 997 MetadataType type = metadataDefinition.getType(); 998 switch (type) 999 { 1000 case COMPOSITE: 1001 if (metadataDefinition instanceof RepeaterDefinition) 1002 { 1003 if (doc == null) 1004 { 1005 return synchronizeRepeaterMetadataFromContent(sharedProgram, sharedMetadataHolder, logicalMetadataPath, logger); 1006 } 1007 else 1008 { 1009 return synchronizeRepeaterMetadataFromDOM(doc, sharedProgramNode, mainProgram, metadataDefinition, sharedMetadataHolder, logicalMetadataPath, catalog, lang, logger); 1010 } 1011 } 1012 else 1013 { 1014 String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée."; 1015 logger.warn(warn); 1016 break; 1017 } 1018 default: 1019 String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée."; 1020 logger.warn(warn); 1021 break; 1022 } 1023 return false; 1024 } 1025 1026 /** 1027 * Synchronize repeater from content 1028 * @param sharedProgram the shared program 1029 * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms 1030 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 1031 * @param logger the logger 1032 * @return <code>true</code> if changes were made 1033 */ 1034 protected boolean synchronizeRepeaterMetadataFromContent(SubProgram sharedProgram, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, Logger logger) 1035 { 1036 if (sharedMetadataHolder.hasMetadata(logicalMetadataPath)) 1037 { 1038 // Remove old values 1039 sharedMetadataHolder.removeMetadata(logicalMetadataPath); 1040 } 1041 1042 if (sharedProgram.getMetadataHolder().hasMetadata(logicalMetadataPath)) 1043 { 1044 ModifiableCompositeMetadata sharedRepeater = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true); 1045 ModifiableCompositeMetadata repeaterToCopy = sharedProgram.getMetadataHolder().getCompositeMetadata(logicalMetadataPath); 1046 repeaterToCopy.copyTo(sharedRepeater); 1047 } 1048 1049 return true; 1050 } 1051 1052 /** 1053 * Synchronize repeater from DOM 1054 * @param doc the document 1055 * @param sharedProgramNode the shared program node 1056 * @param mainProgram the main program 1057 * @param metadataDefinition the metadata definition 1058 * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms 1059 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 1060 * @param catalog the catalog 1061 * @param lang the language 1062 * @param logger the logger 1063 * @return <code>true</code> if changes were made 1064 */ 1065 protected boolean synchronizeRepeaterMetadataFromDOM(Document doc, Node sharedProgramNode, SubProgram mainProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger) 1066 { 1067 boolean hasChanges = false; 1068 Node metadataNode = _xPathProcessor.selectSingleNode(sharedProgramNode, logicalMetadataPath); 1069 if (metadataNode != null) 1070 { 1071 ModifiableCompositeMetadata repeater = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true); 1072 1073 // Remove locale entries (unable to compare locales and remotes ...) 1074 String[] metadataNames = repeater.getMetadataNames(); 1075 for (String entryName : metadataNames) 1076 { 1077 repeater.removeMetadata(entryName); 1078 hasChanges = true; 1079 } 1080 1081 ModifiableCompositeMetadata repeaterMetadataHolder = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true); 1082 1083 // Create new entries from remote data 1084 NodeList entryNodes = metadataNode.getChildNodes(); 1085 for (int i = 0; i < entryNodes.getLength(); i++) 1086 { 1087 Node entryNode = entryNodes.item(i); 1088 String entryName = entryNode.getAttributes().getNamedItem("name").getTextContent().trim(); 1089 1090 NodeList childNodes = entryNode.getChildNodes(); 1091 for (int j = 0; j < childNodes.getLength(); j++) 1092 { 1093 Node childNode = childNodes.item(j); 1094 String subMetadataName = childNode.getLocalName(); 1095 1096 ModifiableCompositeMetadata entryMetadataHolder = repeaterMetadataHolder.getCompositeMetadata(entryName, true); 1097 hasChanges = synchronizeSimpleMetadataFromDOM(doc, mainProgram, childNode, metadataDefinition.getMetadataDefinition(subMetadataName), logicalMetadataPath + "/" + subMetadataName, entryMetadataHolder, subMetadataName, catalog, lang, logger) || hasChanges; 1098 } 1099 } 1100 } 1101 1102 return hasChanges; 1103 } 1104 1105 /** 1106 * Synchronize a simple metadata from DOM 1107 * @param doc the document 1108 * @param subProgram the subProgram content 1109 * @param metadataNode The metadata DOM node 1110 * @param metadataDefinition the metadata definition 1111 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 1112 * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms 1113 * @param metadataName the metadata name 1114 * @param catalog the catalog 1115 * @param lang the language 1116 * @param logger The logger 1117 * @return <code>true</code> if changes were made 1118 */ 1119 public boolean synchronizeSimpleMetadataFromDOM (Document doc, SubProgram subProgram, Node metadataNode, MetadataDefinition metadataDefinition, String logicalMetadataPath, ModifiableCompositeMetadata sharedMetadataHolder, String metadataName, String catalog, String lang, Logger logger) 1120 { 1121 List<String> metadataValues = null; 1122 ContentType contentType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 1123 1124 MetadataType type = metadataDefinition.getType(); 1125 switch (type) 1126 { 1127 case BINARY: 1128 return _handleBinaryMetadata(metadataNode, subProgram, sharedMetadataHolder, metadataName, false, logger); 1129 case FILE: 1130 return _handleFileMetadata(metadataNode, subProgram, logicalMetadataPath, sharedMetadataHolder, metadataName, false, contentType, logger); 1131 case GEOCODE: 1132 return _handleGeocodeMetadata(metadataNode, subProgram, sharedMetadataHolder, metadataName, false); 1133 case CONTENT: 1134 case STRING: 1135 case LONG: 1136 case DOUBLE: 1137 case REFERENCE: 1138 case BOOLEAN: 1139 if (metadataDefinition.isMultiple()) 1140 { 1141 NodeList itemNodes = metadataNode.getChildNodes(); 1142 metadataValues = new ArrayList<>(); 1143 for (int j = 0; j < itemNodes.getLength(); j++) 1144 { 1145 String metadataValue = itemNodes.item(j).getTextContent().trim(); 1146 if (StringUtils.isNotEmpty(metadataValue)) 1147 { 1148 metadataValues.add(metadataValue); 1149 } 1150 } 1151 } 1152 else 1153 { 1154 String metadataValue = metadataNode.getTextContent().trim(); 1155 if (StringUtils.isNotEmpty(metadataValue)) 1156 { 1157 metadataValues = ImmutableList.of(metadataValue); 1158 } 1159 } 1160 break; 1161 default: 1162 String warn = "Le type de métadonnée '" + type.toString() + "' n'est pas supporté pour la fusion des entrées de repeater de parcours partagés. La métadonnée est ignorée"; 1163 logger.warn(warn); 1164 return false; 1165 } 1166 1167 List<Object> handleMetadataValues = _handleMetadataValues(doc, subProgram, metadataDefinition, metadataValues, lang, catalog, logger); 1168 Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(subProgram, contentType, logicalMetadataPath, sharedMetadataHolder, metadataName, handleMetadataValues, false, false, logger); 1169 if (resultMap.getOrDefault("error", Boolean.FALSE)) 1170 { 1171 _nbError++; 1172 } 1173 return resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue(); 1174 } 1175}