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.ByteArrayInputStream; 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.URL; 024import java.net.URLDecoder; 025import java.text.ParseException; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Objects; 035import java.util.Set; 036 037import javax.jcr.RepositoryException; 038import javax.xml.transform.TransformerException; 039 040import org.apache.avalon.framework.activity.Initializable; 041import org.apache.avalon.framework.component.Component; 042import org.apache.avalon.framework.configuration.Configurable; 043import org.apache.avalon.framework.configuration.Configuration; 044import org.apache.avalon.framework.configuration.ConfigurationException; 045import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 046import org.apache.avalon.framework.context.ContextException; 047import org.apache.avalon.framework.context.Contextualizable; 048import org.apache.avalon.framework.service.ServiceException; 049import org.apache.avalon.framework.service.ServiceManager; 050import org.apache.avalon.framework.service.Serviceable; 051import org.apache.cocoon.Constants; 052import org.apache.cocoon.ProcessingException; 053import org.apache.cocoon.environment.Context; 054import org.apache.commons.io.IOUtils; 055import org.apache.commons.lang3.StringUtils; 056import org.apache.excalibur.xml.dom.DOMParser; 057import org.apache.excalibur.xml.xpath.PrefixResolver; 058import org.apache.excalibur.xml.xpath.XPathProcessor; 059import org.slf4j.Logger; 060import org.w3c.dom.Document; 061import org.w3c.dom.Element; 062import org.w3c.dom.NamedNodeMap; 063import org.w3c.dom.Node; 064import org.w3c.dom.NodeList; 065import org.xml.sax.InputSource; 066import org.xml.sax.SAXException; 067 068import org.ametys.cms.ObservationConstants; 069import org.ametys.cms.content.external.ExternalizableMetadataHelper; 070import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus; 071import org.ametys.cms.contenttype.ContentType; 072import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 073import org.ametys.cms.contenttype.MetadataDefinition; 074import org.ametys.cms.contenttype.MetadataType; 075import org.ametys.cms.contenttype.RepeaterDefinition; 076import org.ametys.cms.repository.ContentQueryHelper; 077import org.ametys.cms.repository.ContentTypeExpression; 078import org.ametys.cms.repository.LanguageExpression; 079import org.ametys.cms.repository.ModifiableContent; 080import org.ametys.cms.repository.ModifiableDefaultContent; 081import org.ametys.cms.repository.comment.DefaultCommentManagerExtensionPoint; 082import org.ametys.core.observation.Event; 083import org.ametys.core.observation.ObservationManager; 084import org.ametys.core.user.CurrentUserProvider; 085import org.ametys.odf.ProgramItem; 086import org.ametys.odf.catalog.CatalogsManager; 087import org.ametys.odf.cdmfr.CDMHelper; 088import org.ametys.odf.course.Course; 089import org.ametys.odf.course.CourseFactory; 090import org.ametys.odf.courselist.CourseList; 091import org.ametys.odf.courselist.CourseListFactory; 092import org.ametys.odf.enumeration.OdfReferenceTableEntry; 093import org.ametys.odf.enumeration.OdfReferenceTableHelper; 094import org.ametys.odf.observation.OdfObservationConstants; 095import org.ametys.odf.orgunit.OrgUnit; 096import org.ametys.odf.orgunit.OrgUnitFactory; 097import org.ametys.odf.orgunit.RootOrgUnitProvider; 098import org.ametys.odf.person.PersonFactory; 099import org.ametys.odf.program.ContainerFactory; 100import org.ametys.odf.program.ProgramFactory; 101import org.ametys.odf.program.ProgramPart; 102import org.ametys.odf.program.SubProgramFactory; 103import org.ametys.odf.program.TraversableProgramPart; 104import org.ametys.odf.translation.TranslationHelper; 105import org.ametys.plugins.contentio.ContentImporterHelper; 106import org.ametys.plugins.contentio.synchronize.BaseSynchroComponent; 107import org.ametys.plugins.odfsync.cdmfr.CDMFrSyncExtensionPoint; 108import org.ametys.plugins.odfsync.cdmfr.transformers.CDMFrSyncTransformer; 109import org.ametys.plugins.repository.AmetysObjectIterable; 110import org.ametys.plugins.repository.AmetysObjectResolver; 111import org.ametys.plugins.repository.AmetysRepositoryException; 112import org.ametys.plugins.repository.metadata.CompositeMetadata; 113import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata; 114import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 115import org.ametys.plugins.repository.metadata.ModifiableFile; 116import org.ametys.plugins.repository.metadata.ModifiableResource; 117import org.ametys.plugins.repository.metadata.ModifiableRichText; 118import org.ametys.plugins.repository.query.expression.AndExpression; 119import org.ametys.plugins.repository.query.expression.Expression; 120import org.ametys.plugins.repository.query.expression.Expression.Operator; 121import org.ametys.plugins.repository.query.expression.StringExpression; 122import org.ametys.runtime.config.Config; 123 124import com.google.common.collect.ImmutableList; 125import com.google.common.collect.ImmutableMap; 126 127/** 128 * Abstract class of a component to import a CDM-fr input stream. 129 */ 130public abstract class AbstractImportCDMFrComponent implements ImportCDMFrComponent, Serviceable, Initializable, Contextualizable, Configurable, Component 131{ 132 /** Tag to identify a program */ 133 protected static final String _TAG_PROGRAM = "program"; 134 135 /** Tag to identify a subprogram */ 136 protected static final String _TAG_SUBPROGRAM = "subProgram"; 137 138 /** Tag to identify a container */ 139 protected static final String _TAG_CONTAINER = "container"; 140 141 /** Tag to identify a courseList */ 142 protected static final String _TAG_COURSELIST = "coursesReferences"; 143 144 private static final ContentWorkflowDescription _PROGRAM_WF_DESCRIPTION = new ContentWorkflowDescription(ProgramFactory.PROGRAM_CONTENT_TYPE, "program", 1, 4); 145 private static final ContentWorkflowDescription _SUBPROGRAM_WF_DESCRIPTION = new ContentWorkflowDescription(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, "subprogram", 1, 4); 146 private static final ContentWorkflowDescription _CONTAINER_WF_DESCRIPTION = new ContentWorkflowDescription(ContainerFactory.CONTAINER_CONTENT_TYPE, "container", 1, 4); 147 private static final ContentWorkflowDescription _COURSELIST_WF_DESCRIPTION = new ContentWorkflowDescription(CourseListFactory.COURSE_LIST_CONTENT_TYPE, "courselist", 1, 4); 148 private static final ContentWorkflowDescription _COURSE_WF_DESCRIPTION = new ContentWorkflowDescription(CourseFactory.COURSE_CONTENT_TYPE, "course", 1, 4); 149 private static final ContentWorkflowDescription _ORGUNIT_WF_DESCRIPTION = new ContentWorkflowDescription(OrgUnitFactory.ORGUNIT_CONTENT_TYPE, "orgunit", 1, 4); 150 private static final ContentWorkflowDescription _PERSON_WF_DESCRIPTION = new ContentWorkflowDescription(PersonFactory.PERSON_CONTENT_TYPE, "person", 1, 4); 151 152 private static final PrefixResolver _PREFIX_RESOLVER = new DocbookPrefixResolver(); 153 154 /** The Cocoon context */ 155 protected Context _cocoonContext; 156 157 /** The DOM parser */ 158 protected DOMParser _domParser; 159 160 /** The XPath processor */ 161 protected XPathProcessor _xPathProcessor; 162 163 /** Extension point to transform CDM-fr */ 164 protected CDMFrSyncExtensionPoint _cdmFrSyncExtensionPoint; 165 166 /** Default language configured for ODF */ 167 protected String _odfLang; 168 169 /** The catalog manager */ 170 protected CatalogsManager _catalogsManager; 171 172 /** The ametys object resolver */ 173 protected AmetysObjectResolver _resolver; 174 175 /** The ODF TableRef Helper */ 176 protected OdfReferenceTableHelper _odfRefTableHelper; 177 178 /** The content type extension point */ 179 protected ContentTypeExtensionPoint _contentTypeEP; 180 181 /** The current user provider */ 182 protected CurrentUserProvider _currentUserProvider; 183 184 /** The observation manager */ 185 protected ObservationManager _observationManager; 186 187 /** The root orgunit provider */ 188 protected RootOrgUnitProvider _rootOUProvider; 189 190 /** The base SCC component */ 191 protected BaseSynchroComponent _synchroComponent; 192 193 /** List of synchronized contents */ 194 protected Map<String, Integer> _importedContents; 195 196 /** List of synchronized contents having differences */ 197 protected Set<String> _synchronizedContents; 198 199 /** List of synchronized contents (to avoid a treatment twice or more) */ 200 protected Set<String> _updatedContents; 201 202 /** Number of errors encountered */ 203 protected int _nbError; 204 /** Number of created contents */ 205 protected int _nbCreatedContents; 206 /** Number of synchronized contents */ 207 protected int _nbSynchronizedContents; 208 /** Number of unchanged contents */ 209 protected int _nbNotChangedContents; 210 /** The prefix of the contents */ 211 protected String _contentPrefix; 212 /** Synchronized fields by content type */ 213 protected Map<String, Set<String>> _syncFieldsByContentType; 214 215 public void initialize() throws Exception 216 { 217 _odfLang = Config.getInstance().getValueAsString("odf.programs.lang"); 218 } 219 220 @Override 221 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 222 { 223 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 224 } 225 226 public void configure(Configuration configuration) throws ConfigurationException 227 { 228 _parseSynchronizedFields(); 229 } 230 231 @Override 232 public void service(ServiceManager manager) throws ServiceException 233 { 234 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 235 _domParser = (DOMParser) manager.lookup(DOMParser.ROLE); 236 _xPathProcessor = (XPathProcessor) manager.lookup(XPathProcessor.ROLE); 237 _cdmFrSyncExtensionPoint = (CDMFrSyncExtensionPoint) manager.lookup(CDMFrSyncExtensionPoint.ROLE); 238 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 239 _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 240 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 241 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 242 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 243 _rootOUProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 244 _synchroComponent = (BaseSynchroComponent) manager.lookup(BaseSynchroComponent.ROLE); 245 } 246 247 @Override 248 public String getIdField() 249 { 250 return "cdmfrSyncCode"; 251 } 252 253 /** 254 * Get the synchronized metadata from the configuration file 255 * @throws ConfigurationException if the configuration is not valid. 256 */ 257 private void _parseSynchronizedFields() throws ConfigurationException 258 { 259 _syncFieldsByContentType = new HashMap<>(); 260 261 @SuppressWarnings("resource") 262 InputStream is = null; 263 try 264 { 265 File apogeeMapping = new File(_cocoonContext.getRealPath("/WEB-INF/param/odf-synchro.xml")); 266 if (!apogeeMapping.isFile()) 267 { 268 is = getClass().getResourceAsStream("/org/ametys/plugins/odfsync/cdmfr/odf-synchro.xml"); 269 } 270 else 271 { 272 is = new FileInputStream(apogeeMapping); 273 } 274 Configuration cfg = new DefaultConfigurationBuilder().build(is); 275 276 Configuration[] cTypesConf = cfg.getChildren("content-type"); 277 for (Configuration cTypeConf : cTypesConf) 278 { 279 String contentType = cTypeConf.getAttribute("id"); 280 Set<String> syncMetadata = _configureSynchronizedFields(cTypeConf, ""); 281 _syncFieldsByContentType.put(contentType, syncMetadata); 282 } 283 } 284 catch (Exception e) 285 { 286 throw new ConfigurationException("Error while parsing odf-synchro.xml", e); 287 } 288 finally 289 { 290 IOUtils.closeQuietly(is); 291 } 292 } 293 294 private Set<String> _configureSynchronizedFields(Configuration configuration, String prefix) throws ConfigurationException 295 { 296 Set<String> syncMetadata = new HashSet<>(); 297 Configuration[] metaConf = configuration.getChildren("metadata"); 298 299 if (metaConf.length > 0) 300 { 301 for (Configuration metadata : metaConf) 302 { 303 if (metadata.getChildren("metadata").length > 0) 304 { 305 // composite 306 syncMetadata.addAll(_configureSynchronizedFields(metadata, prefix + metadata.getAttribute("name") + "/")); 307 } 308 else 309 { 310 syncMetadata.add(prefix + metadata.getAttribute("name")); 311 } 312 } 313 } 314 else if (configuration.getAttribute("name", null) != null) 315 { 316 syncMetadata.add(prefix + configuration.getAttribute("name")); 317 } 318 319 return syncMetadata; 320 } 321 322 @Override 323 @SuppressWarnings("unchecked") 324 public synchronized Map<String, Object> handleInputStream(InputStream input, Map<String, Object> parameters, Logger logger) throws ProcessingException 325 { 326 List<ModifiableDefaultContent> importedPrograms = new ArrayList<>(); 327 328 _importedContents = (Map<String, Integer>) parameters.getOrDefault("importedContents", new HashMap<>()); 329 _synchronizedContents = (Set<String>) parameters.getOrDefault("synchronizedContents", new HashSet<>()); 330 _updatedContents = (Set<String>) parameters.getOrDefault("updatedContents", new HashSet<>()); 331 _nbCreatedContents = (int) parameters.getOrDefault("nbCreatedContents", 0); 332 _nbSynchronizedContents = (int) parameters.getOrDefault("nbSynchronizedContents", 0); 333 _nbNotChangedContents = (int) parameters.getOrDefault("nbNotChangedContents", 0); 334 _nbError = (int) parameters.getOrDefault("nbError", 0); 335 _contentPrefix = (String) parameters.getOrDefault("contentPrefix", "cdmfr-"); 336 additionalParameters(parameters); 337 338 Map<String, Object> resultMap = new HashMap<>(); 339 340 try 341 { 342 Document doc = _domParser.parseDocument(new InputSource(input)); 343 doc = transformDocument(doc, new HashMap<String, Object>(), logger); 344 345 if (doc != null) 346 { 347 String defaultLang = _getXPathString(doc, "CDM/@language", _odfLang); 348 349 NodeList nodes = doc.getElementsByTagName(_TAG_PROGRAM); 350 351 for (int i = 0; i < nodes.getLength(); i++) 352 { 353 Node contentNode = nodes.item(i); 354 String syncCode = _xPathProcessor.evaluateAsString(contentNode, "@CDMid"); 355 String contentLang = _getXPathString(contentNode, "@language", defaultLang); 356 contentLang = StringUtils.substring(contentLang, 0, 2).toLowerCase(); // on keep the language from the locale 357 358 String catalog = getCatalogName(contentNode); 359 360 importedPrograms.add(_importOrSynchronizeContent(doc, contentNode, getProgramWfDescription(), syncCode, contentLang, catalog, syncCode, logger)); 361 } 362 363 // Apply changes (synchronize action) 364 for (String contentId : _synchronizedContents) 365 { 366 ModifiableDefaultContent content = _resolver.resolveById(contentId); 367 _synchroComponent.applyChanges(content, BaseSynchroComponent.SYNCHRONIZE_WORKFLOW_ACTION_ID, org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, logger); 368 } 369 370 // Validate contents 371 if (validateAfterImport()) 372 { 373 for (String contentId : _importedContents.keySet()) 374 { 375 ModifiableDefaultContent content = _resolver.resolveById(contentId); 376 _synchroComponent.validateContent(content, _importedContents.get(contentId), logger); 377 } 378 } 379 } 380 } 381 catch (IOException | ProcessingException e) 382 { 383 throw new ProcessingException("An error occured while transforming the stream.", e); 384 } 385 catch (SAXException e) 386 { 387 throw new ProcessingException("An error occured while parsing the stream.", e); 388 } 389 catch (RepositoryException e) 390 { 391 throw new ProcessingException("An error occured while applying changes on contents.", e); 392 } 393 394 resultMap.put("importedContents", _importedContents); 395 resultMap.put("synchronizedContents", _synchronizedContents); 396 resultMap.put("updatedContents", _updatedContents); 397 resultMap.put("nbCreatedContents", _nbCreatedContents); 398 resultMap.put("nbSynchronizedContents", _nbSynchronizedContents); 399 resultMap.put("nbNotChangedContents", _nbNotChangedContents); 400 resultMap.put("nbError", _nbError); 401 resultMap.put("importedPrograms", importedPrograms); 402 403 return resultMap; 404 } 405 406 /** 407 * True to validate the contents after import 408 * @return True to validate the contents after import 409 */ 410 protected abstract boolean validateAfterImport(); 411 412 /** 413 * When returns true, a content created by a previous synchro will be removed if it does not exist anymore during the current synchro. 414 * @return true if a content created by a previous synchro has to be removed if it does not exist anymore during the current synchro. 415 */ 416 protected abstract boolean removalSync(); 417 418 /** 419 * Transform the document depending of it structure. 420 * @param document Document to transform. 421 * @param parameters Optional parameters for transformation 422 * @param logger The logger 423 * @return The transformed document. 424 * @throws IOException if an error occurs. 425 * @throws SAXException if an error occurs. 426 * @throws ProcessingException if an error occurs. 427 */ 428 protected Document transformDocument(Document document, Map<String, Object> parameters, Logger logger) throws IOException, SAXException, ProcessingException 429 { 430 CDMFrSyncTransformer transformer = _cdmFrSyncExtensionPoint.getTransformer(document); 431 if (transformer == null) 432 { 433 logger.error("Cannot match a CDM-fr transformer to this file structure."); 434 return null; 435 } 436 437 return transformer.transform(document, parameters); 438 } 439 440 /** 441 * Get the name of catalog to use for import 442 * @param contentNode The node of program 443 * @return The catalog to used 444 */ 445 protected String getCatalogName(Node contentNode) 446 { 447 String defaultCatalog = _catalogsManager.getDefaultCatalogName(); 448 449 String contentCatalog = _getXPathString(contentNode, "catalog", defaultCatalog); 450 if (_catalogsManager.getCatalog(contentCatalog) == null) 451 { 452 // Catalog is empty or do not exist, use the default catalog 453 return defaultCatalog; 454 } 455 456 return contentCatalog; 457 } 458 459 /** 460 * Get or create the content from the synchronization code, the lang, the catalog and the content type. 461 * @param title The title 462 * @param lang The lang 463 * @param catalog The catalog 464 * @param syncCode The synchronization code 465 * @param wfDescription The workflow description 466 * @param logger The logger 467 * @return the retrieved or created content 468 * @throws RepositoryException if an error occurs 469 */ 470 protected ModifiableDefaultContent _getOrCreateContent(String title, String lang, String catalog, String syncCode, ContentWorkflowDescription wfDescription, Logger logger) throws RepositoryException 471 { 472 List<Expression> expList = getExpressionsList(lang, syncCode, wfDescription.getContentType(), catalog); 473 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 474 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 475 476 AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(xPathQuery); 477 478 if (contents.getSize() > 0) 479 { 480 return contents.iterator().next(); 481 } 482 483 Map<String, Object> resultMap = _synchroComponent.createContentAction(wfDescription.getContentType(), wfDescription.getWorkflowName(), wfDescription.getInitialActionId(), lang, title, _contentPrefix, logger); 484 if ((boolean) resultMap.getOrDefault("error", false)) 485 { 486 _nbError++; 487 } 488 489 ModifiableDefaultContent content = (ModifiableDefaultContent) resultMap.get("content"); 490 491 if (content != null) 492 { 493 ExternalizableMetadataHelper.setMetadata(content.getMetadataHolder(), getIdField(), syncCode); 494 if (catalog != null && content instanceof ProgramItem) 495 { 496 ExternalizableMetadataHelper.setMetadata(content.getMetadataHolder(), ProgramItem.METADATA_CATALOG, catalog); 497 } 498 additionalOperationsBeforeSave(content, logger); 499 content.saveChanges(); 500 _importedContents.put(content.getId(), wfDescription.getValidationActionId()); 501 _nbCreatedContents++; 502 } 503 return content; 504 } 505 506 /** 507 * Additional parameters for specific treatments. 508 * @param parameters The parameters map to get 509 */ 510 protected abstract void additionalParameters(Map<String, Object> parameters); 511 512 /** 513 * Additional operation to do on the content before saving it. 514 * @param content The content 515 * @param logger The logger 516 * @throws RepositoryException if an error occurs 517 */ 518 protected abstract void additionalOperationsBeforeSave(ModifiableDefaultContent content, Logger logger) throws RepositoryException; 519 520 /** 521 * Import or synchronize the content. 522 * @param doc XML document 523 * @param contentNode Node of the content 524 * @param wfDescription The workflow description 525 * @param title The title 526 * @param lang The lang 527 * @param catalog The catalog 528 * @param syncCode The synchronization code 529 * @param logger The logger 530 * @return The imported or synchronized content 531 */ 532 protected ModifiableDefaultContent _importOrSynchronizeContent(Document doc, Node contentNode, ContentWorkflowDescription wfDescription, String title, String lang, String catalog, String syncCode, Logger logger) 533 { 534 ModifiableDefaultContent content = null; 535 try 536 { 537 content = _getOrCreateContent(title, lang, catalog, syncCode, wfDescription, logger); 538 if (content != null) 539 { 540 _synchronizeContent(doc, contentNode, content, wfDescription.getContentType(), lang, catalog, syncCode, logger); 541 } 542 } 543 catch (RepositoryException e) 544 { 545 _nbError++; 546 logger.error("An error occurred while importing or synchronizing content", e); 547 } 548 return content; 549 } 550 551 /** 552 * Synchronize content 553 * @param doc The root document 554 * @param contentNode the DOM content node 555 * @param content The content to synchronize 556 * @param contentTypeId The content type ID 557 * @param lang Parent program language (to select the good courses in the CDM-FR file) 558 * @param catalog The catalog of parent program 559 * @param syncCode The synchronization code 560 * @param logger The logger 561 */ 562 protected void _synchronizeContent(Document doc, Node contentNode, ModifiableDefaultContent content, String contentTypeId, String lang, String catalog, String syncCode, Logger logger) 563 { 564 if (_updatedContents.add(content.getId())) 565 { 566 logger.info("Synchronization of the content '{}' with the content type '{}'", content.getTitle(), contentTypeId); 567 568 List<ModifiableDefaultContent> children = new LinkedList<>(); 569 570 if (content.isLocked()) 571 { 572 logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), content.getLockOwner()); 573 } 574 else 575 { 576 boolean hasChanges = false; 577 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 578 int courseListPosition = 0; 579 580 NodeList metadataNodes = contentNode.getChildNodes(); 581 for (int i = 0; i < metadataNodes.getLength(); i++) 582 { 583 Node metadataNode = metadataNodes.item(i); 584 String metadataName = metadataNode.getLocalName(); 585 586 if (metadataName.equals(_TAG_SUBPROGRAM)) 587 { 588 String subContentSyncCode = _xPathProcessor.evaluateAsString(metadataNode, "@CDMid"); 589 String title = _xPathProcessor.evaluateAsString(metadataNode, "title"); 590 ModifiableDefaultContent subProgram = _importOrSynchronizeContent(doc, metadataNode, getSubProgramWfDescription(), title, lang, catalog, subContentSyncCode, logger); 591 if (subProgram != null) 592 { 593 children.add(subProgram); 594 } 595 } 596 else if (metadataName.equals(_TAG_CONTAINER)) 597 { 598 String subContentSyncCode = _xPathProcessor.evaluateAsString(metadataNode, "@CDMid"); 599 String title = _xPathProcessor.evaluateAsString(metadataNode, "title"); 600 ModifiableDefaultContent container = _importOrSynchronizeContent(doc, metadataNode, getContainerWfDescription(), title, lang, catalog, subContentSyncCode, logger); 601 if (container != null) 602 { 603 children.add(container); 604 } 605 } 606 else if (metadataName.equals(_TAG_COURSELIST)) 607 { 608 courseListPosition++; 609 // For courseList from another source than Ametys, there is no unique code, then a code is generated with the parent ID and the position in the parent : [parentId]-[position] 610 String subContentSyncCode = _getXPathString(metadataNode, "@code", syncCode + "-" + courseListPosition); 611 String title = _getXPathString(metadataNode, "@name", "Liste d'éléments pédagogiques"); 612 ModifiableDefaultContent courseList = _importOrSynchronizeContent(doc, metadataNode, getCourseListWfDescription(), title, lang, catalog, subContentSyncCode, logger); 613 if (courseList != null) 614 { 615 _synchronizeCourseList(doc, metadataNode, courseList, lang, catalog, logger); 616 children.add(courseList); 617 } 618 } 619 // Explicitely ignore catalog metadata which is forced at content creation 620 else if (!metadataName.equals(ProgramItem.METADATA_CATALOG)) 621 { 622 hasChanges = _synchronizeMetadata(doc, metadataNode, content, metadataName, metadataName, contentType, lang, logger) || hasChanges; 623 } 624 } 625 626 hasChanges = _setRelations(content, children, logger) || hasChanges; 627 628 // Create translation links 629 _linkTranslationsIfExist(content, contentTypeId); 630 631 _saveContentChanges(content, contentTypeId, hasChanges, logger); 632 } 633 } 634 } 635 636 /** 637 * Synchronize a course list, it has attributes to synchronize. 638 * @param doc The XML document 639 * @param courseListNode The XML node of the course list 640 * @param courseList The course list content 641 * @param lang The lang 642 * @param catalog The catalog 643 * @param logger The logger 644 */ 645 protected void _synchronizeCourseList(Document doc, Node courseListNode, ModifiableDefaultContent courseList, String lang, String catalog, Logger logger) 646 { 647 boolean hasChanges = false; 648 649 ContentType contentType = _contentTypeEP.getExtension(getCourseListWfDescription().getContentType()); 650 NamedNodeMap attributes = courseListNode.getAttributes(); 651 for (int i = 0; i < attributes.getLength(); i++) 652 { 653 Node attributeNode = attributes.item(i); 654 String attributeName = attributeNode.getLocalName(); 655 // Explicitely ignore catalog metadata which is forced at content creation 656 if (!attributeName.equals(ProgramItem.METADATA_CATALOG)) 657 { 658 hasChanges = _synchronizeMetadata(doc, attributeNode, courseList, attributeName, attributeName, contentType, lang, logger) || hasChanges; 659 } 660 } 661 662 List<ModifiableDefaultContent> courses = new LinkedList<>(); 663 NodeList itemNodes = courseListNode.getChildNodes(); 664 for (int i = 0; i < itemNodes.getLength(); i++) 665 { 666 String syncCode = itemNodes.item(i).getTextContent().trim(); 667 668 Node courseNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "course[@CDMid = '" + syncCode + "' and @language = '" + lang + "']"); 669 if (courseNode == null) 670 { 671 courseNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "course[@CDMid = '" + syncCode + "']"); 672 } 673 674 if (courseNode != null) 675 { 676 String elpLang = _getXPathString(courseNode, "@language", lang); 677 678 // Check the catalog is the same as the parent program 679 String courseCatalog = getCatalogName(courseNode); 680 if (courseCatalog != null && !courseCatalog.equals(catalog)) 681 { 682 String elpCode = _xPathProcessor.evaluateAsString(courseNode, "elpCode"); 683 logger.error("The course '{}' belongs to a different catalog than the one from the imported/synchronized program : '{}' vs '{}'. No synchronization will be done on this course.", elpCode, courseCatalog, catalog); 684 } 685 else 686 { 687 String title = _xPathProcessor.evaluateAsString(courseNode, "title"); 688 ModifiableDefaultContent course = _importOrSynchronizeContent(doc, courseNode, getCourseWfDescription(), title, elpLang, catalog, syncCode, logger); 689 if (course != null) 690 { 691 courses.add(course); 692 } 693 } 694 } 695 } 696 697 if (!courses.isEmpty()) 698 { 699 hasChanges = ExternalizableMetadataHelper.setMetadata(courseList.getMetadataHolder(), CourseList.METADATA_CHILD_COURSES, courses.toArray(new ModifiableDefaultContent[courses.size()])) || hasChanges; 700 for (ModifiableDefaultContent course : courses) 701 { 702 hasChanges = _synchroComponent.updateRelation(course.getMetadataHolder(), Course.METADATA_PARENT_COURSE_LISTS, courseList, false) || hasChanges; 703 } 704 } 705 706 _saveContentChanges(courseList, getCourseListWfDescription().getContentType(), hasChanges, logger); 707 } 708 709 private void _fetchImages(Node docbookNode, ModifiableDefaultContent content, ModifiableRichText richText, String metadataPath, Logger logger) 710 { 711 NodeList unfetchUrls = _xPathProcessor.selectNodeList(docbookNode, ".//docbook:mediaobject/docbook:imageobject/docbook:imagedata", _PREFIX_RESOLVER); 712 713 for (int i = 0; i < unfetchUrls.getLength(); i++) 714 { 715 Element href = (Element) unfetchUrls.item(i); 716 String imageUrl = href.getAttribute("fileref"); 717 718 if (DefaultCommentManagerExtensionPoint.URL_VALIDATOR.matcher(imageUrl).matches()) 719 { 720 @SuppressWarnings("resource") 721 InputStream is = null; 722 try 723 { 724 URL url = new URL(imageUrl); 725 is = url.openStream(); 726 727 String path = url.getPath(); 728 String fileName = path.substring(path.lastIndexOf("/") + 1); 729 730 ModifiableFile file = null; 731 if (richText.getAdditionalDataFolder().hasFile(fileName)) 732 { 733 file = richText.getAdditionalDataFolder().getFile(fileName); 734 } 735 else 736 { 737 file = richText.getAdditionalDataFolder().addFile(fileName); 738 } 739 740 ModifiableResource resource = file.getResource(); 741 resource.setInputStream(is); 742 resource.setLastModified(new Date()); 743 744 String mimeType = _cocoonContext.getMimeType(fileName); 745 if (mimeType == null) 746 { 747 mimeType = "application/unknown"; 748 } 749 resource.setMimeType(mimeType); 750 751 String fetchUrl = content.getId() + "@" + metadataPath + ";" + fileName; 752 href.setAttribute("fileref", fetchUrl); 753 } 754 catch (Exception e) 755 { 756 logger.warn("Unable to retrieve remote image '{}'.", imageUrl, e); 757 } 758 finally 759 { 760 IOUtils.closeQuietly(is); 761 } 762 } 763 } 764 } 765 766 /** 767 * Synchronize a metadata (can be a composite or a repeater). 768 * @param doc The XML document 769 * @param metadataNode The metadata node 770 * @param content The content 771 * @param logicalMetadataPath The logical metadata path (to retrieve the definition) 772 * @param completeMetadataPath The complete metadata path (to retrieve the metadata holder) 773 * @param contentType The content type 774 * @param lang The lang 775 * @param logger The logger 776 * @return <code>true</code> if changes occurs 777 */ 778 protected boolean _synchronizeMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger) 779 { 780 List<String> metadataValues = null; 781 MetadataDefinition metadataDef = contentType.getMetadataDefinitionByPath(logicalMetadataPath); 782 Map<String, Object> params = ImmutableMap.of("contentType", contentType.getId()); 783 boolean synchronize = getLocalAndExternalFields(params).contains(logicalMetadataPath); 784 if (metadataDef != null) 785 { 786 if (metadataDef instanceof RepeaterDefinition) 787 { 788 return _handleRepeaterMetadata(doc, metadataNode, content, logicalMetadataPath, completeMetadataPath, contentType, lang, logger); 789 } 790 else if (metadataDef.getType() == MetadataType.COMPOSITE) 791 { 792 return _handleCompositeMetadata(doc, metadataNode, content, logicalMetadataPath, completeMetadataPath, contentType, lang, logger); 793 } 794 else if (metadataDef.getType() == MetadataType.RICH_TEXT) 795 { 796 return _handleRichTextMetadata(content, logicalMetadataPath, completeMetadataPath, metadataNode, synchronize, logger); 797 } 798 else if (metadataDef.getType() == MetadataType.BINARY) 799 { 800 return _handleBinaryMetadata(metadataNode, content, completeMetadataPath, synchronize, logger); 801 } 802 else if (metadataDef.getType() == MetadataType.FILE) 803 { 804 return _handleFileMetadata(metadataNode, content, logicalMetadataPath, completeMetadataPath, synchronize, contentType, logger); 805 } 806 else if (metadataDef.getType() == MetadataType.GEOCODE) 807 { 808 return _handleGeocodeMetadata(metadataNode, content, completeMetadataPath, synchronize); 809 } 810 else if (metadataDef.getType() == MetadataType.BOOLEAN) 811 { 812 metadataValues = ImmutableList.of("true"); 813 } 814 else if (metadataDef.isMultiple()) 815 { 816 NodeList itemNodes = metadataNode.getChildNodes(); 817 metadataValues = new ArrayList<>(); 818 for (int j = 0; j < itemNodes.getLength(); j++) 819 { 820 String metadataValue = itemNodes.item(j).getTextContent().trim(); 821 if (StringUtils.isNotEmpty(metadataValue)) 822 { 823 metadataValues.add(metadataValue); 824 } 825 } 826 } 827 else 828 { 829 String metadataValue = metadataNode.getTextContent().trim(); 830 if (StringUtils.isNotEmpty(metadataValue)) 831 { 832 metadataValues = ImmutableList.of(metadataValue); 833 } 834 } 835 } 836 837 Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(content, contentType, logicalMetadataPath, completeMetadataPath, _handleMetadataValues(doc, content, metadataDef, metadataValues, lang, logger), synchronize, _importedContents.containsKey(content.getId()), logger); 838 if (resultMap.getOrDefault("error", Boolean.FALSE)) 839 { 840 _nbError++; 841 } 842 return resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue(); 843 } 844 845 private boolean _handleGeocodeMetadata(Node metadataNode, ModifiableDefaultContent content, String completeMetadataPath, boolean synchronize) 846 { 847 ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath); 848 String[] arrayPath = completeMetadataPath.split("/"); 849 String metadataName = arrayPath[arrayPath.length - 1]; 850 851 if (!metadataNode.hasChildNodes()) 852 { 853 return _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize); 854 } 855 856 boolean hasChanges = false; 857 ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL; 858 ModifiableCompositeMetadata geoCode = ExternalizableMetadataHelper.getCompositeMetadata(metadataHolder, metadataName, status, true); 859 if (_importedContents.containsKey(content.getId())) 860 { 861 hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges; 862 } 863 864 NodeList coordinates = metadataNode.getChildNodes(); 865 for (int j = 0; j < coordinates.getLength(); j++) 866 { 867 Node coordinate = coordinates.item(j); 868 hasChanges = ExternalizableMetadataHelper.setMetadata(geoCode, coordinate.getLocalName(), coordinate.getTextContent().trim()) || hasChanges; 869 } 870 return hasChanges; 871 } 872 873 private boolean _handleFileMetadata(Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, boolean synchronize, ContentType contentType, Logger logger) 874 { 875 ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath); 876 String[] arrayPath = completeMetadataPath.split("/"); 877 String metadataName = arrayPath[arrayPath.length - 1]; 878 879 boolean hasChanges = false; 880 String value = metadataNode.getTextContent().trim(); 881 boolean metadataExists = metadataHolder.hasMetadata(metadataName); 882 CompositeMetadata.MetadataType metadataType = metadataExists ? metadataHolder.getType(metadataName) : null; 883 if (DefaultCommentManagerExtensionPoint.URL_VALIDATOR.matcher(value).matches()) 884 { 885 if (metadataExists && metadataType != CompositeMetadata.MetadataType.BINARY) 886 { 887 hasChanges = _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize) || hasChanges; 888 } 889 hasChanges = _handleBinaryMetadata(metadataNode, content, completeMetadataPath, synchronize, logger) || hasChanges; 890 } 891 else 892 { 893 if (metadataExists && metadataType != CompositeMetadata.MetadataType.STRING) 894 { 895 hasChanges = _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize) || hasChanges; 896 } 897 898 Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(content, contentType, logicalMetadataPath, completeMetadataPath, ImmutableList.of(value), synchronize, _importedContents.containsKey(content.getId()), logger); 899 if (resultMap.getOrDefault("error", Boolean.FALSE)) 900 { 901 _nbError++; 902 } 903 hasChanges = resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue() || hasChanges; 904 } 905 return hasChanges; 906 } 907 908 private boolean _handleBinaryMetadata(Node metadataNode, ModifiableDefaultContent content, String completeMetadataPath, boolean synchronize, Logger logger) 909 { 910 ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath); 911 String[] arrayPath = completeMetadataPath.split("/"); 912 String metadataName = arrayPath[arrayPath.length - 1]; 913 914 boolean hasChanges = false; 915 916 String value = metadataNode.getTextContent().trim(); 917 918 if (StringUtils.isEmpty(value)) 919 { 920 return _synchroComponent.removeMetadataIfExists(metadataHolder, completeMetadataPath, synchronize); 921 } 922 923 @SuppressWarnings("resource") 924 InputStream is = null; 925 try 926 { 927 URL url = new URL(value); 928 is = url.openStream(); 929 930 ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL; 931 ModifiableBinaryMetadata binaryMetadata = ExternalizableMetadataHelper.getBinaryMetadata(metadataHolder, metadataName, status, true); 932 933 byte[] bytes = IOUtils.toByteArray(is); 934 byte[] oldValue = new byte[0]; 935 try 936 { 937 oldValue = IOUtils.toByteArray(binaryMetadata.getInputStream()); 938 } 939 catch (AmetysRepositoryException e) 940 { 941 logger.debug("The old value of '{}' should not be initialized on the content '{}'.", completeMetadataPath, content.getId(), e); 942 } 943 944 if (!Objects.deepEquals(bytes, oldValue)) 945 { 946 String path = url.getPath(); 947 String filename = path.substring(path.lastIndexOf("/") + 1); 948 String mimeType = _cocoonContext.getMimeType(filename); 949 if (mimeType == null) 950 { 951 mimeType = "application/unknown"; 952 } 953 binaryMetadata.setFilename(URLDecoder.decode(filename, "UTF-8")); 954 binaryMetadata.setMimeType(mimeType); 955 binaryMetadata.setLastModified(new Date()); 956 binaryMetadata.setInputStream(is); 957 958 hasChanges = true; 959 } 960 961 if (_importedContents.containsKey(content.getId())) 962 { 963 hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges; 964 } 965 } 966 catch (IOException e) 967 { 968 logger.error("Unable to retrieve remote file input stream", e); 969 } 970 finally 971 { 972 IOUtils.closeQuietly(is); 973 } 974 975 return hasChanges; 976 } 977 978 private boolean _handleRepeaterMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger) 979 { 980 boolean hasChanges = false; 981 982 ModifiableCompositeMetadata repeater = content.getMetadataHolder().getCompositeMetadata(completeMetadataPath, true); 983 984 // Remove locale entries (unable to compare locales and remotes ...) 985 String[] metadataNames = repeater.getMetadataNames(); 986 for (String entryName : metadataNames) 987 { 988 repeater.removeMetadata(entryName); 989 hasChanges = true; 990 } 991 992 // Create new entries from remote data 993 NodeList entryNodes = metadataNode.getChildNodes(); 994 for (int i = 0; i < entryNodes.getLength(); i++) 995 { 996 Node entryNode = entryNodes.item(i); 997 String entryName = entryNode.getAttributes().getNamedItem("name").getTextContent().trim(); 998 999 NodeList childNodes = entryNode.getChildNodes(); 1000 for (int j = 0; j < childNodes.getLength(); j++) 1001 { 1002 Node childNode = childNodes.item(j); 1003 String subMetadataName = childNode.getLocalName(); 1004 1005 hasChanges = _synchronizeMetadata(doc, childNode, content, logicalMetadataPath + "/" + subMetadataName, completeMetadataPath + "/" + entryName + "/" + subMetadataName, contentType, lang, logger) || hasChanges; 1006 } 1007 } 1008 1009 return hasChanges; 1010 } 1011 1012 private boolean _handleCompositeMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger) 1013 { 1014 boolean hasChanges = false; 1015 NodeList subMetadataNodes = metadataNode.getChildNodes(); 1016 for (int i = 0; i < subMetadataNodes.getLength(); i++) 1017 { 1018 Node subMetadataNode = subMetadataNodes.item(i); 1019 hasChanges = _synchronizeMetadata(doc, subMetadataNode, content, logicalMetadataPath + "/" + subMetadataNode.getLocalName(), completeMetadataPath + "/" + subMetadataNode.getLocalName(), contentType, lang, logger) || hasChanges; 1020 } 1021 return hasChanges; 1022 } 1023 1024 private boolean _handleRichTextMetadata(ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, Node metadataNode, boolean synchronize, Logger logger) 1025 { 1026 ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath); 1027 String[] arrayPath = completeMetadataPath.split("/"); 1028 String metadataName = arrayPath[arrayPath.length - 1]; 1029 1030 if (metadataNode.hasChildNodes()) 1031 { 1032 boolean hasChanges = false; 1033 try 1034 { 1035 String docbook = ContentImporterHelper.serializeNode(metadataNode.getFirstChild()); 1036 try (ByteArrayInputStream is = new ByteArrayInputStream(docbook.getBytes("UTF-8"))) 1037 { 1038 ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL; 1039 ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(metadataHolder, metadataName, status, true); 1040 1041 _fetchImages(metadataNode.getFirstChild(), content, richText, completeMetadataPath, logger); 1042 richText.setInputStream(is); 1043 richText.setMimeType("text/xml"); 1044 richText.setLastModified(new Date()); 1045 1046 if (_importedContents.containsKey(content.getId())) 1047 { 1048 hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges; 1049 } 1050 1051 hasChanges = true; 1052 } 1053 catch (IOException e) 1054 { 1055 logger.error("An error occured while parsing the rich text '{}' of the content '{}'", logicalMetadataPath, content.getTitle(), e); 1056 } 1057 } 1058 catch (TransformerException e) 1059 { 1060 logger.error("Error serializing a docbook node.", e); 1061 } 1062 return hasChanges; 1063 } 1064 1065 return ExternalizableMetadataHelper.removeMetadataIfExists(content.getMetadataHolder(), completeMetadataPath); 1066 } 1067 1068 private List<Object> _handleMetadataValues(Document doc, ModifiableDefaultContent content, MetadataDefinition metadataDef, List<String> metadataValues, String lang, Logger logger) 1069 { 1070 if (metadataValues == null) 1071 { 1072 return null; 1073 } 1074 1075 List<Object> finalMetadataValues = new ArrayList<>(); 1076 for (String metadataValue : metadataValues) 1077 { 1078 switch (metadataDef.getType()) 1079 { 1080 case DATE: 1081 try 1082 { 1083 finalMetadataValues.add(CDMHelper.CDM_DATE_FORMAT.parse(metadataValue)); 1084 } 1085 catch (ParseException e) 1086 { 1087 logger.warn("Unable to parse the metadata '{}' of the content '{}' with the value '{}', it should respect the following format '{}'.", metadataDef.getName(), content.getId(), metadataValue, CDMHelper.CDM_DATE_FORMAT); 1088 } 1089 break; 1090 case BOOLEAN: 1091 finalMetadataValues.add(Boolean.TRUE); 1092 break; 1093 case CONTENT: 1094 String refContentTypeId = metadataDef.getContentType(); 1095 if (refContentTypeId != null) 1096 { 1097 ContentType refContentType = _contentTypeEP.getExtension(refContentTypeId); 1098 if (refContentTypeId.equals(PersonFactory.PERSON_CONTENT_TYPE)) 1099 { 1100 Node personNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "person[@CDMid ='" + metadataValue + "']"); 1101 ModifiableDefaultContent person = _importOrSynchronizeContent(doc, personNode, getPersonWfDescription(), metadataValue, lang, null, metadataValue, logger); 1102 if (person != null) 1103 { 1104 finalMetadataValues.add(person); 1105 } 1106 } 1107 else if (refContentTypeId.equals(OrgUnitFactory.ORGUNIT_CONTENT_TYPE)) 1108 { 1109 Node ouNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "orgunit[@CDMid ='" + metadataValue + "']"); 1110 ModifiableDefaultContent orgUnit = _importOrSynchronizeContent(doc, ouNode, getOrgUnitWfDescription(), metadataValue, lang, null, metadataValue, logger); 1111 if (orgUnit != null) 1112 { 1113 finalMetadataValues.add(orgUnit); 1114 } 1115 } 1116 else if (refContentType.isReferenceTable() && _odfRefTableHelper.isTableReference(refContentTypeId)) 1117 { 1118 String entryId = _getIdFromCDMThenCode(refContentTypeId, metadataValue); 1119 if (StringUtils.isNotEmpty(entryId)) 1120 { 1121 finalMetadataValues.add(_resolver.resolveById(entryId)); 1122 } 1123 else 1124 { 1125 logger.warn("There is not entry corresponding to the CDM-fr or Ametys code '{}' in the reference table '{}'.", metadataValue, refContentTypeId); 1126 } 1127 } 1128 } 1129 else 1130 { 1131 logger.warn("Cannot match data '{}' of content type '{}' because it is not typed.", metadataDef.getName(), metadataDef.getReferenceContentType()); 1132 } 1133 break; 1134 default: 1135 if (content instanceof CourseList && metadataDef.getName().equals(CourseList.METADATA_CHOICE_TYPE)) 1136 { 1137 finalMetadataValues.add(metadataValue.toUpperCase()); 1138 } 1139 else 1140 { 1141 finalMetadataValues.add(metadataValue); 1142 } 1143 break; 1144 } 1145 } 1146 1147 return finalMetadataValues; 1148 } 1149 1150 /** 1151 * Search for translated contents 1152 * @param importedContent The imported content 1153 * @param contentType The content type 1154 */ 1155 protected void _linkTranslationsIfExist(ModifiableContent importedContent, String contentType) 1156 { 1157 if (importedContent instanceof ProgramItem) 1158 { 1159 Map<String, String> translations = new HashMap<>(); 1160 1161 List<Expression> expList = getExpressionsList(importedContent.getLanguage(), importedContent.getMetadataHolder().getString(getIdField()), contentType, importedContent.getMetadataHolder().getString(ProgramItem.METADATA_CATALOG, null)); 1162 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 1163 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 1164 1165 AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(xPathQuery); 1166 1167 for (ModifiableDefaultContent content : contents) 1168 { 1169 translations.put(content.getLanguage(), content.getId()); 1170 1171 Map<String, String> translations2 = TranslationHelper.getTranslations(content); 1172 translations2.put(importedContent.getLanguage(), importedContent.getId()); 1173 TranslationHelper.setTranslations(content, translations2); 1174 1175 Map<String, Object> eventParams = new HashMap<>(); 1176 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1177 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1178 _observationManager.notify(new Event(OdfObservationConstants.ODF_CONTENT_TRANSLATED, _currentUserProvider.getUser(), eventParams)); 1179 } 1180 1181 TranslationHelper.setTranslations(importedContent, translations); 1182 } 1183 } 1184 1185 /** 1186 * Save the changes of the content. 1187 * @param content Content to save 1188 * @param contentTypeId The content type (for logs) 1189 * @param hasChanges If there are changes to save 1190 * @param logger The logger 1191 */ 1192 protected void _saveContentChanges(ModifiableDefaultContent content, String contentTypeId, boolean hasChanges, Logger logger) 1193 { 1194 if (hasChanges) 1195 { 1196 content.saveChanges(); 1197 _synchronizedContents.add(content.getId()); 1198 if (!_importedContents.containsKey(content.getId())) 1199 { 1200 _nbSynchronizedContents++; 1201 } 1202 } 1203 else 1204 { 1205 logger.info("No changes detected for content '{}' with the content type '{}'", content.getTitle(), contentTypeId); 1206 if (!_importedContents.containsKey(content.getId())) 1207 { 1208 _nbNotChangedContents++; 1209 } 1210 } 1211 } 1212 1213 /** 1214 * Set relations for the content. 1215 * @param content The content to update 1216 * @param children Children to set 1217 * @param logger The logger 1218 * @return <code>true</code> if changes occurs 1219 */ 1220 protected boolean _setRelations(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, Logger logger) 1221 { 1222 boolean hasChanges = false; 1223 1224 if (!children.isEmpty()) 1225 { 1226 hasChanges = _setChildren(content, children, logger) || hasChanges; 1227 } 1228 1229 // Set the orgUnit parent (if no parent is set) 1230 if (content instanceof OrgUnit) 1231 { 1232 hasChanges = _setOrgUnitParent(content, logger); 1233 } 1234 1235 return hasChanges; 1236 } 1237 1238 /** 1239 * Set children for the given content. 1240 * @param content Content to add the children 1241 * @param children Children to add 1242 * @param logger The logger 1243 * @return <code>true</code> if changes occurs 1244 */ 1245 protected boolean _setChildren(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, Logger logger) 1246 { 1247 boolean hasChanges = false; 1248 1249 String metadataName = null; 1250 String invertMetadataName = null; 1251 if (content instanceof CourseList) 1252 { 1253 metadataName = CourseList.METADATA_CHILD_COURSES; 1254 invertMetadataName = Course.METADATA_PARENT_COURSE_LISTS; 1255 } 1256 else if (content instanceof TraversableProgramPart) 1257 { 1258 metadataName = TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS; 1259 invertMetadataName = ProgramPart.METADATA_PARENT_PROGRAM_PARTS; 1260 } 1261 else if (content instanceof Course) 1262 { 1263 metadataName = Course.METADATA_CHILD_COURSE_LISTS; 1264 invertMetadataName = CourseList.METADATA_PARENT_COURSES; 1265 } 1266 1267 if (metadataName != null) 1268 { 1269 hasChanges = _updateDoubleRelation(content, children, metadataName, invertMetadataName, logger); 1270 } 1271 1272 return hasChanges; 1273 } 1274 1275 /** 1276 * Set the orgUnit parent to rootOrgUnit. 1277 * @param orgUnit The orgunit to link 1278 * @param logger The logger 1279 * @return <code>true</code> if changes occurs 1280 */ 1281 protected boolean _setOrgUnitParent(ModifiableDefaultContent orgUnit, Logger logger) 1282 { 1283 boolean hasChanges = false; 1284 1285 // Set the orgUnit parent (if no parent is set) 1286 ModifiableCompositeMetadata holder = orgUnit.getMetadataHolder(); 1287 if (!holder.hasMetadata(OrgUnit.METADATA_PARENT_ORGUNIT)) 1288 { 1289 OrgUnit rootOrgUnit = _rootOUProvider.getRoot(); 1290 1291 hasChanges = ExternalizableMetadataHelper.setMetadata(holder, OrgUnit.METADATA_PARENT_ORGUNIT, rootOrgUnit) || hasChanges; 1292 hasChanges = _synchroComponent.updateRelation(rootOrgUnit.getMetadataHolder(), OrgUnit.METADATA_CHILD_ORGUNITS, orgUnit, false) || hasChanges; 1293 1294 try 1295 { 1296 _synchroComponent.applyChanges(rootOrgUnit, 22, ObservationConstants.EVENT_CONTENT_MODIFIED, logger); 1297 } 1298 catch (RepositoryException e) 1299 { 1300 logger.error("An error occured during updating root org unit after synchronizing the content '{}'.", orgUnit.getId(), logger); 1301 } 1302 } 1303 1304 return hasChanges; 1305 } 1306 1307 /** 1308 * Get the content ID from the CDM code, if there is no match with the CDM code, then we search with the code. 1309 * If nothing is found we return null. 1310 * @param tableRefId The reference table ID 1311 * @param cdmCode The CDM code 1312 * @return A content ID or <code>null</code> 1313 */ 1314 protected String _getIdFromCDMThenCode(String tableRefId, String cdmCode) 1315 { 1316 OdfReferenceTableEntry entry = _odfRefTableHelper.getItemFromCDM(tableRefId, cdmCode); 1317 if (entry == null) 1318 { 1319 entry = _odfRefTableHelper.getItemFromCode(tableRefId, cdmCode); 1320 } 1321 return entry != null ? entry.getId() : null; 1322 } 1323 1324 private boolean _updateDoubleRelation(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, String metadataName, String invertMetadataName, Logger logger) 1325 { 1326 boolean hasChanges = false; 1327 ModifiableCompositeMetadata holder = content.getMetadataHolder(); 1328 1329 // "Normal" relation 1330 if (removalSync() || !holder.hasMetadata(metadataName)) 1331 { 1332 hasChanges = ExternalizableMetadataHelper.setMetadata(holder, metadataName, children.toArray(new ModifiableDefaultContent[children.size()])); 1333 } 1334 else 1335 { 1336 List<String> oldValues = new ArrayList<>(Arrays.asList(holder.getStringArray(metadataName))); 1337 1338 for (ModifiableDefaultContent child : children) 1339 { 1340 hasChanges = _synchroComponent.updateRelation(holder, metadataName, child, false) || hasChanges; 1341 oldValues.remove(child.getId()); 1342 } 1343 1344 if (logger.isWarnEnabled()) 1345 { 1346 for (String oldValue : oldValues) 1347 { 1348 // Warn the old contents 1349 ModifiableDefaultContent child = _resolver.resolveById(oldValue); 1350 logger.warn("The ODF content '{}' ({}) is not linked anymore to the content '{}', it should be manually removed.", child.getTitle(), oldValue, content.getTitle()); 1351 } 1352 } 1353 } 1354 1355 // Invert relation 1356 for (ModifiableDefaultContent child : children) 1357 { 1358 hasChanges = _synchroComponent.updateRelation(child.getMetadataHolder(), invertMetadataName, content, false) || hasChanges; 1359 } 1360 1361 return hasChanges; 1362 } 1363 1364 private String _getXPathString(Node metadataNode, String xPath, String defaultValue) 1365 { 1366 String value = _xPathProcessor.evaluateAsString(metadataNode, xPath); 1367 if (StringUtils.isEmpty(value)) 1368 { 1369 value = defaultValue; 1370 } 1371 return value; 1372 } 1373 1374 /** 1375 * Get the program workflow description. 1376 * @return A {@link ContentWorkflowDescription} containing informations about the program workflow 1377 */ 1378 protected ContentWorkflowDescription getProgramWfDescription() 1379 { 1380 return _PROGRAM_WF_DESCRIPTION; 1381 } 1382 1383 /** 1384 * Get the subprogram workflow description. 1385 * @return A {@link ContentWorkflowDescription} containing informations about the subprogram workflow 1386 */ 1387 protected ContentWorkflowDescription getSubProgramWfDescription() 1388 { 1389 return _SUBPROGRAM_WF_DESCRIPTION; 1390 } 1391 1392 /** 1393 * Get the container workflow description. 1394 * @return A {@link ContentWorkflowDescription} containing informations about the container workflow 1395 */ 1396 protected ContentWorkflowDescription getContainerWfDescription() 1397 { 1398 return _CONTAINER_WF_DESCRIPTION; 1399 } 1400 1401 /** 1402 * Get the course list workflow description. 1403 * @return A {@link ContentWorkflowDescription} containing informations about the course list workflow 1404 */ 1405 protected ContentWorkflowDescription getCourseListWfDescription() 1406 { 1407 return _COURSELIST_WF_DESCRIPTION; 1408 } 1409 1410 /** 1411 * Get the course workflow description. 1412 * @return A {@link ContentWorkflowDescription} containing informations about the course workflow 1413 */ 1414 protected ContentWorkflowDescription getCourseWfDescription() 1415 { 1416 return _COURSE_WF_DESCRIPTION; 1417 } 1418 1419 /** 1420 * Get the orgunit workflow description. 1421 * @return A {@link ContentWorkflowDescription} containing informations about the orgunit workflow 1422 */ 1423 protected ContentWorkflowDescription getOrgUnitWfDescription() 1424 { 1425 return _ORGUNIT_WF_DESCRIPTION; 1426 } 1427 1428 /** 1429 * Get the person workflow description. 1430 * @return A {@link ContentWorkflowDescription} containing informations about the person workflow 1431 */ 1432 protected ContentWorkflowDescription getPersonWfDescription() 1433 { 1434 return _PERSON_WF_DESCRIPTION; 1435 } 1436 1437 /** 1438 * Internal object to describe content workflow elements. 1439 */ 1440 protected static class ContentWorkflowDescription 1441 { 1442 private String _contentType; 1443 private String _workflowName; 1444 private int _initialActionId; 1445 private int _validationActionId; 1446 1447 ContentWorkflowDescription(String contentType, String workflowName, int initialActionId, int validationActionId) 1448 { 1449 _contentType = contentType; 1450 _workflowName = workflowName; 1451 _initialActionId = initialActionId; 1452 _validationActionId = validationActionId; 1453 } 1454 1455 /** 1456 * Get the content type. 1457 * @return the content type ID 1458 */ 1459 public String getContentType() 1460 { 1461 return _contentType; 1462 } 1463 1464 /** 1465 * Get the workflow name. 1466 * @return the workflow name 1467 */ 1468 public String getWorkflowName() 1469 { 1470 return _workflowName; 1471 } 1472 1473 /** 1474 * Get the initial action ID. 1475 * @return the initial action ID 1476 */ 1477 public int getInitialActionId() 1478 { 1479 return _initialActionId; 1480 } 1481 1482 /** 1483 * Get the validation action ID. 1484 * @return the validation action ID 1485 */ 1486 public int getValidationActionId() 1487 { 1488 return _validationActionId; 1489 } 1490 } 1491 1492 private static class DocbookPrefixResolver implements PrefixResolver 1493 { 1494 private Map<String, String> _ns = new HashMap<>(); 1495 1496 public DocbookPrefixResolver() 1497 { 1498 _ns.put("docbook", "http://docbook.org/ns/docbook"); 1499 } 1500 1501 @Override 1502 public String prefixToNamespace(String prefix) 1503 { 1504 return _ns.get(prefix); 1505 } 1506 } 1507 1508 @Override 1509 public List<Expression> getExpressionsList(String lang, String idValue, String contentType, String catalog) 1510 { 1511 List<Expression> expList = new ArrayList<>(); 1512 1513 if (StringUtils.isNotBlank(contentType)) 1514 { 1515 expList.add(new ContentTypeExpression(Operator.EQ, contentType)); 1516 } 1517 1518 if (StringUtils.isNotBlank(idValue)) 1519 { 1520 expList.add(new StringExpression(getIdField(), Operator.EQ, idValue)); 1521 } 1522 1523 if (StringUtils.isNotBlank(catalog)) 1524 { 1525 expList.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalog)); 1526 } 1527 1528 if (StringUtils.isNotBlank(lang)) 1529 { 1530 expList.add(new LanguageExpression(Operator.EQ, lang)); 1531 } 1532 1533 return expList; 1534 } 1535 1536 @Override 1537 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 1538 { 1539 if (additionalParameters == null || !additionalParameters.containsKey("contentType")) 1540 { 1541 throw new IllegalArgumentException("Content type shouldn't be null."); 1542 } 1543 1544 String contentType = additionalParameters.get("contentType").toString(); 1545 Set<String> syncFields = _syncFieldsByContentType.get(contentType); 1546 if (syncFields == null) 1547 { 1548 syncFields = new HashSet<>(); 1549 _syncFieldsByContentType.put(contentType, syncFields); 1550 } 1551 return syncFields; 1552 } 1553}