001/* 002 * Copyright 2016 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.contentio.synchronize; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.stream.Collectors; 026 027import javax.jcr.RepositoryException; 028import javax.jcr.Value; 029import javax.mail.MessagingException; 030 031import org.apache.avalon.framework.configuration.Configurable; 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.lang3.StringUtils; 038import org.slf4j.Logger; 039 040import org.ametys.cms.FilterNameHelper; 041import org.ametys.cms.ObservationConstants; 042import org.ametys.cms.content.external.ExternalizableMetadataHelper; 043import org.ametys.cms.contenttype.ContentType; 044import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 045import org.ametys.cms.contenttype.ContentTypesHelper; 046import org.ametys.cms.contenttype.MetadataDefinition; 047import org.ametys.cms.languages.Language; 048import org.ametys.cms.languages.LanguagesManager; 049import org.ametys.cms.repository.Content; 050import org.ametys.cms.repository.ContentDAO; 051import org.ametys.cms.repository.ContentQueryHelper; 052import org.ametys.cms.repository.ContentTypeExpression; 053import org.ametys.cms.repository.DefaultContent; 054import org.ametys.cms.repository.LanguageExpression; 055import org.ametys.cms.repository.ModifiableDefaultContent; 056import org.ametys.cms.repository.WorkflowAwareContent; 057import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 058import org.ametys.cms.workflow.CreateContentFunction; 059import org.ametys.core.observation.Event; 060import org.ametys.core.observation.ObservationManager; 061import org.ametys.core.user.CurrentUserProvider; 062import org.ametys.core.util.I18nUtils; 063import org.ametys.core.util.mail.SendMailHelper; 064import org.ametys.plugins.contentio.synchronize.expression.CollectionExpression; 065import org.ametys.plugins.repository.AmetysObjectIterable; 066import org.ametys.plugins.repository.AmetysObjectResolver; 067import org.ametys.plugins.repository.AmetysRepositoryException; 068import org.ametys.plugins.repository.lock.LockHelper; 069import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 070import org.ametys.plugins.repository.query.expression.AndExpression; 071import org.ametys.plugins.repository.query.expression.Expression; 072import org.ametys.plugins.repository.query.expression.Expression.Operator; 073import org.ametys.plugins.repository.query.expression.StringExpression; 074import org.ametys.plugins.workflow.AbstractWorkflowComponent; 075import org.ametys.plugins.workflow.support.WorkflowProvider; 076import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 077import org.ametys.runtime.config.Config; 078import org.ametys.runtime.i18n.I18nizableText; 079 080import com.opensymphony.workflow.InvalidActionException; 081import com.opensymphony.workflow.WorkflowException; 082import com.opensymphony.workflow.spi.Step; 083 084/** 085 * Abstract implementation of {@link SynchronizableContentsCollection}. 086 * 087 */ 088public abstract class AbstractSynchronizableContentsCollection implements SynchronizableContentsCollection, Configurable, Serviceable 089{ 090 /** The id */ 091 protected String _id; 092 /** The label */ 093 protected I18nizableText _label; 094 /** The path to the metadata holding the 'restricted' property */ 095 protected String _restrictedField; 096 /** The handled content type */ 097 protected String _contentType; 098 /** The id of controller */ 099 protected String _modelId; 100 /** The untyped values of controller's parameters */ 101 protected Map<String, Object> _modelParamValues; 102 /** True if removal sync */ 103 protected boolean _removalSync; 104 /** The name of the workflow */ 105 protected String _workflowName; 106 /** The id of the initial action of the workflow */ 107 protected int _initialActionId; 108 /** The id of the validate action of the workflow */ 109 protected int _validateActionId; 110 /** The prefix of the contents */ 111 protected String _contentPrefix; 112 /** True to validate contents after import */ 113 protected boolean _validateAfterImport; 114 /** The report mails */ 115 protected String _reportMails; 116 /** The id of the content operator to use */ 117 protected String _synchronizingContentOperator; 118 119 /** The languges manager */ 120 protected LanguagesManager _languagesManager; 121 /** The content type extension point */ 122 protected ContentTypeExtensionPoint _contentTypeEP; 123 /** The workflow provider */ 124 protected WorkflowProvider _workflowProvider; 125 /** The ametys object resolver */ 126 protected AmetysObjectResolver _resolver; 127 /** The helper for content types */ 128 protected ContentTypesHelper _contentTypesHelper; 129 /** The current user provider */ 130 protected CurrentUserProvider _currentUserProvider; 131 /** The observation manager */ 132 protected ObservationManager _observationManager; 133 /** The i18n utils */ 134 protected I18nUtils _i18nUtils; 135 /** The extension point for Synchronizing Content Operators */ 136 protected SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP; 137 /** The content DAO */ 138 protected ContentDAO _contentDAO; 139 140 /** Number of errors encountered */ 141 protected int _nbError; 142 /** True if there is a global error during synchronization */ 143 protected boolean _hasGlobalError; 144 private List<String> _handledContents; 145 private int _nbCreatedContents; 146 private int _nbSynchronizedContents; 147 private int _nbNotChangedContents; 148 private int _nbDeletedContents; 149 150 @Override 151 public void configure(Configuration configuration) throws ConfigurationException 152 { 153 _id = configuration.getAttribute("id"); 154 _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), null); 155 _contentType = configuration.getChild("contentType").getValue(); 156 _removalSync = configuration.getChild("removalSync").getValueAsBoolean(false); 157 _workflowName = configuration.getChild("workflowName").getValue(); 158 _initialActionId = configuration.getChild("initialActionId").getValueAsInteger(); 159 _validateActionId = configuration.getChild("validateActionId").getValueAsInteger(); 160 _contentPrefix = configuration.getChild("contentPrefix").getValue(); 161 _restrictedField = configuration.getChild("restrictedField").getValue(null); 162 _validateAfterImport = configuration.getChild("validateAfterImport").getValueAsBoolean(false); 163 _reportMails = configuration.getChild("reportMails").getValue(""); 164 _synchronizingContentOperator = configuration.getChild("contentOperator").getValue(); 165 _modelId = configuration.getChild("model").getAttribute("id"); 166 _modelParamValues = _parseParameters(configuration.getChild("model")); 167 } 168 169 @Override 170 public void service(ServiceManager manager) throws ServiceException 171 { 172 _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 173 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 174 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 175 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 176 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 177 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 178 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 179 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 180 _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) manager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE); 181 _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE); 182 } 183 184 /** 185 * Parse parameters' values 186 * @param configuration The root configuration 187 * @return The parameters 188 * @throws ConfigurationException if an error occurred 189 */ 190 protected Map<String, Object> _parseParameters(Configuration configuration) throws ConfigurationException 191 { 192 Map<String, Object> values = new LinkedHashMap<>(); 193 194 Configuration[] params = configuration.getChildren("param"); 195 for (Configuration paramConfig : params) 196 { 197 values.put(paramConfig.getAttribute("name"), paramConfig.getValue("")); 198 } 199 return values; 200 } 201 202 @Override 203 public String getId() 204 { 205 return _id; 206 } 207 208 @Override 209 public I18nizableText getLabel() 210 { 211 return _label; 212 } 213 214 @Override 215 public String getContentType() 216 { 217 return _contentType; 218 } 219 220 @Override 221 public String getRestrictedField() 222 { 223 return _restrictedField; 224 } 225 226 @Override 227 public String getSynchronizeCollectionModelId() 228 { 229 return _modelId; 230 } 231 232 @Override 233 public Map<String, Object> getParameterValues() 234 { 235 return _modelParamValues; 236 } 237 238 @Override 239 public boolean removalSync() 240 { 241 return _removalSync; 242 } 243 244 @Override 245 public String getWorkflowName() 246 { 247 return _workflowName; 248 } 249 250 @Override 251 public int getInitialActionId() 252 { 253 return _initialActionId; 254 } 255 256 @Override 257 public int getValidateActionId() 258 { 259 return _validateActionId; 260 } 261 262 @Override 263 public String getContentPrefix() 264 { 265 return _contentPrefix; 266 } 267 268 @Override 269 public boolean validateAfterImport() 270 { 271 return _validateAfterImport; 272 } 273 274 @Override 275 public String getReportMails() 276 { 277 return _reportMails; 278 } 279 280 @Override 281 public String getSynchronizingContentOperator() 282 { 283 return _synchronizingContentOperator; 284 } 285 286 @Override 287 public void populate(Logger logger) 288 { 289 _handledContents = new ArrayList<>(); 290 _nbCreatedContents = 0; 291 _nbSynchronizedContents = 0; 292 _nbNotChangedContents = 0; 293 _nbDeletedContents = 0; 294 _nbError = 0; 295 _hasGlobalError = false; 296 297 logger.info("Start synchronization of collection '{}'", _id); 298 long startTime = System.currentTimeMillis(); 299 300 // Do populate 301 _internalPopulate(logger); 302 303 if (!_hasGlobalError && _removalSync) 304 { 305 // Delete old contents if source prevails 306 deleteUnexistingContents(logger); 307 } 308 309 long endTime = System.currentTimeMillis(); 310 logger.info("End synchronization of collection '{}' in {} ms", _id, endTime - startTime); 311 logger.info("{} contents were created", _nbCreatedContents); 312 logger.info("{} contents were updated", _nbSynchronizedContents); 313 logger.info("{} contents did not changed", _nbNotChangedContents); 314 logger.info("{} contents were deleted", _nbDeletedContents); 315 316 Map<String, Object> eventParams = new HashMap<>(); 317 eventParams.put(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.ARGS_COLLECTION_ID, this.getId()); 318 eventParams.put(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.ARGS_COLLECTION_CONTENT_TYPE, this.getContentType()); 319 _observationManager.notify(new Event(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_COLLECTION_SYNCHRONIZED, _currentUserProvider.getUser(), eventParams)); 320 321 if (_nbError > 0 && _reportMails.length() > 0) 322 { 323 try 324 { 325 logger.info("{} contents were not created/updated because of an error.", _nbError); 326 sendErrorMail(_nbError); 327 } 328 catch (MessagingException e) 329 { 330 logger.warn("Unable to send mail", e); 331 } 332 } 333 } 334 335 /** 336 * Sends the report mails 337 * @param nbError The number of error 338 * @throws MessagingException if a messaging error occurred 339 */ 340 protected void sendErrorMail(int nbError) throws MessagingException 341 { 342 StringBuilder recipients = new StringBuilder(); // the builder for the addresses separated by a space 343 for (String recipient : _reportMails.split("\\n")) 344 { 345 if (recipients.length() != 0) 346 { 347 recipients.append(" "); 348 } 349 recipients.append(recipient.trim()); 350 } 351 352 String sender = Config.getInstance().getValueAsString("smtp.mail.from"); 353 354 String pluginName = "plugin.contentio"; 355 List<String> params = new ArrayList<>(); 356 params.add(_id); 357 String subject = _i18nUtils.translate(new I18nizableText(pluginName, "PLUGINS_CONTENTIO_POPULATE_REPORT_MAIL_SUBJECT", params)); 358 359 params.clear(); 360 params.add(String.valueOf(nbError)); 361 params.add(_id); 362 String baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValueAsString("cms.url"), "index.html"), "/"); 363 params.add(baseUrl + "/_admin/index.html?uitool=uitool-admin-logs"); 364 String body = _i18nUtils.translate(new I18nizableText(pluginName, "PLUGINS_CONTENTIO_POPULATE_REPORT_MAIL_BODY", params)); 365 366 SendMailHelper.sendMail(subject, null, body, recipients.toString(), sender); 367 } 368 369 /** 370 * Internal implementation of {@link #populate(Logger)} 371 * @param logger The logger 372 */ 373 protected abstract void _internalPopulate(Logger logger); 374 375 /** 376 * Adds the given content as handled (i.e. will not be removed if {@link #_removalSync} is true) 377 * @param id The id of the content 378 */ 379 protected void _handleContent(String id) 380 { 381 _handledContents.add(id); 382 } 383 384 /** 385 * Returns true if the given content is handled 386 * @param id The content to test 387 * @return true if the given content is handled 388 */ 389 protected boolean _isHandled(String id) 390 { 391 return _handledContents.contains(id); 392 } 393 394 /** 395 * Imports or synchronizes a content for each available language 396 * @param idValue The unique identifier of the content 397 * @param remoteValues The remote values 398 * @param logger The logger 399 */ 400 protected void importContent(String idValue, Map<String, List<Object>> remoteValues, Logger logger) 401 { 402 _handleContent(idValue); 403 404 Map<String, Language> languages = _languagesManager.getAvailableLanguages(); 405 for (String lang : languages.keySet()) 406 { 407 importContent(lang, idValue, remoteValues, logger); 408 } 409 } 410 411 /** 412 * Imports or synchronizes a content for given language 413 * @param lang The language 414 * @param idValue The title of the content 415 * @param remoteValues The remote values 416 * @param logger The logger 417 */ 418 protected void importContent(String lang, String idValue, Map<String, List<Object>> remoteValues, Logger logger) 419 { 420 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(_synchronizingContentOperator); 421 Map<String, List<Object>> transformedRemoteValues = synchronizingContentOperator.transform(remoteValues, logger); 422 423 try 424 { 425 long startTime = System.currentTimeMillis(); 426 427 ContentType contentType = _contentTypeEP.getExtension(_contentType); 428 ModifiableDefaultContent content = _getContent(lang, idValue); 429 if (content == null) 430 { 431 // Create new content 432 logger.info("Start importing content '{}' for language {}", idValue, lang); 433 434 // Content does not exist, create it 435 String contentTitle = idValue; 436 if (transformedRemoteValues.containsKey("title")) 437 { 438 List<Object> remoteTitles = transformedRemoteValues.get("title"); 439 contentTitle = (String) remoteTitles.stream().filter(obj -> obj instanceof String && StringUtils.isNotEmpty((String) obj)).findFirst().orElse(idValue); 440 } 441 442 String contentId = createContentAction(lang, contentTitle, logger); 443 if (contentId != null) 444 { 445 content = _resolver.resolveById(contentId); 446 447 // Synchronize content metadata 448 synchronizeContent(transformedRemoteValues, contentType, content, true, logger); 449 updateSCCProperty(content); 450 451 content.saveChanges(); 452 content.checkpoint(); 453 454 // Validate content if allowed 455 validateContent(content, logger); 456 457 _nbCreatedContents++; 458 459 // Notify a content was imported 460 Map<String, Object> eventParams = new HashMap<>(); 461 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 462 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 463 _observationManager.notify(new Event(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, _currentUserProvider.getUser(), eventParams)); 464 465 long endTime = System.currentTimeMillis(); 466 logger.info("End import of content '{}' for language {} in {} ms", contentId, lang, endTime - startTime); 467 } 468 469 } 470 else 471 { 472 // Update content 473 logger.info("Start synchronizing content '{}' for language {}", content.getTitle(), lang); 474 475 _ensureTitleIsPresent(content, transformedRemoteValues, logger); 476 477 if (synchronizeContent(transformedRemoteValues, contentType, content, false, logger)) 478 { 479 boolean success = applyChanges(content, logger); 480 if (success) 481 { 482 _nbSynchronizedContents++; 483 logger.info("Some changes were detected for content '{}' and language {}", content.getTitle(), lang); 484 } 485 } 486 else 487 { 488 _nbNotChangedContents++; 489 logger.info("No changes detected for content '{}' and language {}", content.getTitle(), lang); 490 } 491 492 long endTime = System.currentTimeMillis(); 493 logger.info("End synchronization of content '{}' for language {} in {} ms", content.getTitle(), lang, endTime - startTime); 494 } 495 496 if (content != null) 497 { 498 // Do additional operation on the content 499 synchronizingContentOperator.additionalOperation(content, transformedRemoteValues, logger); 500 } 501 502 } 503 catch (Exception e) 504 { 505 _nbError++; 506 logger.error("An error occurred while importing or synchronizing content", e); 507 } 508 } 509 510 private void _ensureTitleIsPresent(Content content, Map<String, List<Object>> remoteValues, Logger logger) 511 { 512 if (remoteValues.containsKey("title")) 513 { 514 List<Object> titleValues = remoteValues.get("title"); 515 boolean atLeastOneTitle = titleValues.stream() 516 .filter(String.class::isInstance) 517 .map(String.class::cast) 518 .anyMatch(StringUtils::isNotBlank); 519 if (atLeastOneTitle) 520 { 521 return; 522 } 523 } 524 525 // Force to current title 526 logger.warn("The remote value of 'title' is empty for the content {}. The 'title' metadata is mandatory, the current title will remain.", content); 527 remoteValues.put("title", Collections.singletonList(content.getTitle())); 528 } 529 530 /** 531 * Add the current synchronizable collection as property 532 * @param content The synchronized content 533 * @throws RepositoryException if an error occurred 534 */ 535 protected void updateSCCProperty (DefaultContent content) throws RepositoryException 536 { 537 if (content.getNode().hasProperty(COLLECTION_ID_PROPERTY)) 538 { 539 Value[] values = content.getNode().getProperty(COLLECTION_ID_PROPERTY).getValues(); 540 List<String> collectionIds = new ArrayList<>(); 541 for (Value value : values) 542 { 543 collectionIds.add(value.getString()); 544 } 545 collectionIds.add(getId()); 546 547 content.getNode().setProperty(COLLECTION_ID_PROPERTY, collectionIds.toArray(new String[] {})); 548 } 549 else 550 { 551 content.getNode().setProperty(COLLECTION_ID_PROPERTY, new String[] {getId()}); 552 } 553 } 554 555 /** 556 * Gets the content in the repository 557 * @param lang the language 558 * @param idValue the content name 559 * @return the content in the repository, or null if does not exist 560 */ 561 protected ModifiableDefaultContent _getContent(String lang, String idValue) 562 { 563 String query = _getContentPathQuery(lang, idValue, _contentType); 564 AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(query); 565 566 if (contents.getSize() > 0) 567 { 568 return contents.iterator().next(); 569 } 570 return null; 571 } 572 573 private String _getContentPathQuery(String lang, String idValue, String contentType) 574 { 575 List<Expression> expList = new ArrayList<>(); 576 577 CollectionExpression collectionExpr = new CollectionExpression(_id); 578 expList.add(collectionExpr); 579 580 if (StringUtils.isNotBlank(contentType)) 581 { 582 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, contentType); 583 expList.add(cTypeExpr); 584 } 585 586 if (StringUtils.isNotBlank(idValue)) 587 { 588 StringExpression stringExp = new StringExpression(getIdField(), Operator.EQ, idValue); 589 expList.add(stringExp); 590 } 591 592 if (StringUtils.isNotBlank(lang)) 593 { 594 LanguageExpression langExp = new LanguageExpression(Operator.EQ, lang); 595 expList.add(langExp); 596 } 597 598 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 599 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 600 601 return xPathQuery; 602 } 603 604 /** 605 * Creates content action with result from request 606 * @param lang The language 607 * @param contentTitle The content title 608 * @param logger The logger 609 * @return The content id, or null of a workflow error occured 610 */ 611 protected String createContentAction(String lang, String contentTitle, Logger logger) 612 { 613 String contentName = _getContentName(contentTitle, lang); 614 logger.info("Creating content '{}' for language {}", contentName, lang); 615 616 Map<String, Object> inputs = new HashMap<>(); 617 618 inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, lang); 619 inputs.put(CreateContentFunction.CONTENT_NAME_KEY, contentName); 620 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle); 621 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {_contentType}); 622 623 Map<String, Object> results = new HashMap<>(); 624 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 625 626 try 627 { 628 _workflowProvider.getAmetysObjectWorkflow().initialize(_workflowName, _initialActionId, inputs); 629 @SuppressWarnings("unchecked") 630 Map<String, Object> workflowResult = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY); 631 return (String) workflowResult.get("contentId"); 632 } 633 catch (WorkflowException e) 634 { 635 _nbError++; 636 logger.error("Failed to initialize workflow for content " + contentTitle + " and language " + lang, e); 637 return null; 638 } 639 } 640 641 /** 642 * Gets the content name 643 * @param title The name 644 * @param lang The lang of the content 645 * @return The content name 646 */ 647 protected String _getContentName(String title, String lang) 648 { 649 return FilterNameHelper.filterName(_contentPrefix + "-" + title + "-" + lang); 650 } 651 652 /** 653 * Synchronizes content 654 * @param remoteValues The remote values 655 * @param contentType The content type 656 * @param content The content to synchronize 657 * @param create true if content is creating, false if it is updated 658 * @param logger The logger 659 * @return true if changes were made 660 */ 661 protected boolean synchronizeContent(Map<String, List<Object>> remoteValues, ContentType contentType, ModifiableDefaultContent content, boolean create, Logger logger) 662 { 663 boolean hasChanges = false; 664 665 if (content.isLocked()) 666 { 667 logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), content.getLockOwner()); 668 } 669 else 670 { 671 for (String metadataPath : remoteValues.keySet()) 672 { 673 boolean synchronize = getLocalAndExternalFields().contains(metadataPath); 674 675 MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinitionByMetadataValuePath(metadataPath, new String[] {contentType.getId()}, new String[] {}); 676 List<Object> remoteValue = remoteValues.get(metadataPath); 677 678 ModifiableCompositeMetadata metadataHolder = _getMetadataHolder(content.getMetadataHolder(), metadataPath); 679 String[] arrayPath = metadataPath.split("/"); 680 String metadataName = arrayPath[arrayPath.length - 1]; 681 682 if (metadataDef != null && remoteValue != null && !remoteValue.isEmpty()) 683 { 684 Object valueToSet; 685 if (metadataDef.isMultiple()) 686 { 687 valueToSet = remoteValue.toArray(); 688 } 689 else 690 { 691 valueToSet = remoteValue.get(0); // remoteValue is not empty at this stage 692 } 693 694 hasChanges = _setMetadata(metadataHolder, metadataName, valueToSet, synchronize, create) || hasChanges; 695 } 696 else if (metadataDef != null && metadataDef.getDefaultValue() != null) 697 { 698 hasChanges = _setMetadata(metadataHolder, metadataName, metadataDef.getDefaultValue(), synchronize, create) || hasChanges; 699 } 700 else 701 { 702 hasChanges = _removeMetadataIfExists(metadataHolder, metadataName, synchronize) || hasChanges; 703 } 704 } 705 } 706 707 return hasChanges; 708 } 709 710 private boolean _removeMetadataIfExists(ModifiableCompositeMetadata metadataHolder, String metadataName, boolean synchronize) 711 { 712 if (synchronize) 713 { 714 return ExternalizableMetadataHelper.removeExternalMetadataIfExists(metadataHolder, metadataName); 715 } 716 else 717 { 718 boolean hasMetadata = metadataHolder.hasMetadata(metadataName); 719 if (hasMetadata) 720 { 721 metadataHolder.removeMetadata(metadataName); 722 } 723 return hasMetadata; 724 } 725 } 726 727 private ModifiableCompositeMetadata _getMetadataHolder(ModifiableCompositeMetadata parentMetadata, String metadataPath) 728 { 729 int pos = metadataPath.indexOf("/"); 730 if (pos == -1) 731 { 732 return parentMetadata; 733 } 734 else 735 { 736 return _getMetadataHolder(parentMetadata.getCompositeMetadata(metadataPath.substring(0, pos), true), metadataPath.substring(pos + 1)); 737 } 738 } 739 740 private boolean _setMetadata(ModifiableCompositeMetadata metadataHolder, String metadataName, Object valueToSet, boolean synchronize, boolean forceExternalStatus) 741 { 742 if (synchronize) 743 { 744 return ExternalizableMetadataHelper.setExternalMetadata(metadataHolder, metadataName, valueToSet, forceExternalStatus); 745 } 746 else 747 { 748 return ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, valueToSet); 749 } 750 } 751 752 /** 753 * Validates a content after import 754 * @param content The content to validate 755 * @param logger The logger 756 */ 757 protected void validateContent(WorkflowAwareContent content, Logger logger) 758 { 759 if (!_validateAfterImport) 760 { 761 // Direct validation is not allowed 762 return; 763 } 764 765 Map<String, Object> inputs = new HashMap<>(); 766 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<>()); 767 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 768 // inputs.put(ValidateSynchronizedContentFunction.SILENTLY, true); 769 770 try 771 { 772 _workflowProvider.getAmetysObjectWorkflow(content).doAction(content.getWorkflowId(), _validateActionId, inputs); 773 logger.info("The content {} ({}) has been validated after import", content.getTitle(), content.getId()); 774 } 775 catch (InvalidActionException e) 776 { 777 logger.error(String.format("The content %s (%s) cannot be validated after import: may miss mandatory metadata ?", content.getTitle(), content.getId()), e); 778 } 779 catch (WorkflowException e) 780 { 781 logger.error(String.format("The content %s (%s) cannot be validated after import", content.getTitle(), content.getId()), e); 782 } 783 } 784 785 /** 786 * Does workflow action 787 * @param content The synchronized content 788 * @param logger The logger 789 * @return true if the content is considered as synchronized (the apply succeeded), false otherwise. 790 * @throws RepositoryException if an error occurs when trying to rollback pending changes in the repository. 791 */ 792 protected boolean applyChanges(ModifiableDefaultContent content, Logger logger) throws RepositoryException 793 { 794 try 795 { 796 content.setLastModified(new Date()); 797 try 798 { 799 content.saveChanges(); 800 } 801 catch (AmetysRepositoryException e) 802 { 803 _nbError++; 804 logger.error(String.format("An error occurred while saving changes on content '%s'.", content.getId()), e); 805 806 // Rollback pending changes 807 content.getNode().getSession().refresh(false); 808 return false; 809 } 810 811 // Create new version 812 content.checkpoint(); 813 814 // Notify observers that the content has been modified 815 Map<String, Object> eventParams = new HashMap<>(); 816 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 817 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 818 _observationManager.notify(new Event(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, _currentUserProvider.getUser(), eventParams)); 819 820 if (content.isLocked() && !LockHelper.isLockOwner(content, _currentUserProvider.getUser())) 821 { 822 logger.warn("Cannot apply changes because content {} is currently locked by ", content.getTitle(), _currentUserProvider.getUser()); 823 return true; 824 } 825 826 Map<String, Object> inputs = new HashMap<>(); 827 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<>()); 828 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 829 830 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 831 Step currentStep = (Step) workflow.getCurrentSteps(content.getWorkflowId()).get(0); 832 int currentStepId = currentStep.getStepId(); 833 834 int actionId = 800 + currentStepId * 10; // 810, 820, 830, etc. 835 workflow.doAction(content.getWorkflowId(), actionId, inputs); 836 } 837 catch (WorkflowException e) 838 { 839 _nbError++; 840 logger.error(String.format("Unable to update workflow of content %s (%s)", content.getTitle(), content.getId()), e); 841 } 842 843 return true; 844 } 845 846 /** 847 * Delete contents created by a previous synchronization which does not exist anymore in remote source 848 * @param logger The logger 849 */ 850 @SuppressWarnings("unchecked") 851 protected void deleteUnexistingContents(Logger logger) 852 { 853 String query = _getContentPathQuery(null, null, null); 854 AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(query); 855 856 List<Content> contentsToRemove = contents.stream() 857 .filter(content -> !_isHandled(_getIdFieldValue(content))) 858 .collect(Collectors.toList()); 859 860 contentsToRemove.stream().forEach(content -> logger.info("The content '{}' ({}) does not exist anymore in remote source: it will be deleted if possible.", content.getTitle(), content.getId())); 861 862 logger.info("Trying to delete contents. This can take a while..."); 863 Map<String, Object> result = _contentDAO.deleteContents(contentsToRemove.stream().map(Content::getId).collect(Collectors.toList()), true); 864 logger.info("Contents deleting process ended."); 865 866 List<Map<String, Object>> deletedContents = (List) result.get("deleted-contents"); 867 _nbDeletedContents += deletedContents.size(); 868 869 List<Map<String, Object>> referencedContents = (List) result.get("referenced-contents"); 870 if (referencedContents.size() > 0) 871 { 872 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 873 } 874 875 List<Map<String, Object>> lockedContents = (List) result.get("locked-contents"); 876 if (lockedContents.size() > 0) 877 { 878 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 879 } 880 881 List<Map<String, Object>> undeletedContents = (List) result.get("undeleted-contents"); 882 if (undeletedContents.size() > 0) 883 { 884 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 885 } 886 } 887 888 /** 889 * Get the value of metadata holding the unique identifier of the synchronized content 890 * @param content The content 891 * @return The value 892 */ 893 protected String _getIdFieldValue(DefaultContent content) 894 { 895 ModifiableCompositeMetadata metadataHolder = _getMetadataHolder(content.getMetadataHolder(), getIdField()); 896 897 String[] pathSegments = getIdField().split("/"); 898 String metadataName = pathSegments[pathSegments.length - 1]; 899 900 return metadataHolder.getString(metadataName, null); 901 } 902}