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