001/* 002 * Copyright 2017 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.Arrays; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import org.apache.avalon.framework.configuration.Configuration; 032import org.apache.avalon.framework.configuration.ConfigurationException; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.commons.lang3.StringUtils; 036import org.slf4j.Logger; 037 038import org.ametys.cms.content.ContentHelper; 039import org.ametys.cms.contenttype.ContentType; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ModifiableContent; 042import org.ametys.cms.repository.WorkflowAwareContent; 043import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 044import org.ametys.cms.workflow.EditContentFunction; 045import org.ametys.core.schedule.progression.ContainerProgressionTracker; 046import org.ametys.core.schedule.progression.ProgressionTrackerFactory; 047import org.ametys.core.schedule.progression.SimpleProgressionTracker; 048import org.ametys.core.util.MapUtils; 049import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction; 050import org.ametys.plugins.repository.AmetysObjectIterable; 051import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 052import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 053import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 054import org.ametys.plugins.repository.data.holder.values.ValueContext; 055import org.ametys.plugins.repository.version.VersionableAmetysObject; 056import org.ametys.plugins.workflow.AbstractWorkflowComponent; 057import org.ametys.runtime.i18n.I18nizableText; 058import org.ametys.runtime.model.View; 059 060import com.opensymphony.workflow.WorkflowException; 061 062/** 063 * Abstract implementation of {@link SynchronizableContentsCollection}. 064 */ 065public abstract class AbstractSimpleSynchronizableContentsCollection extends AbstractSynchronizableContentsCollection 066{ 067 /** The extension point for Synchronizing Content Operators */ 068 protected SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP; 069 070 /** The content helper */ 071 protected ContentHelper _contentHelper; 072 073 private List<String> _handledContents; 074 075 @Override 076 public void service(ServiceManager manager) throws ServiceException 077 { 078 super.service(manager); 079 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 080 _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) manager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE); 081 } 082 083 @Override 084 public void configure(Configuration configuration) throws ConfigurationException 085 { 086 super.configure(configuration); 087 _handledContents = new ArrayList<>(); 088 } 089 090 @Override 091 public List<ModifiableContent> populate(Logger logger, ContainerProgressionTracker progressionTracker) 092 { 093 _handledContents.clear(); 094 List<ModifiableContent> populatedContents = super.populate(logger, progressionTracker); 095 _handledContents.clear(); 096 return populatedContents; 097 } 098 099 @Override 100 protected List<ModifiableContent> _internalPopulate(Logger logger, ContainerProgressionTracker progressionTracker) 101 { 102 return _importOrSynchronizeContents(new HashMap<>(), false, logger, progressionTracker); 103 } 104 105 /** 106 * Adds the given content as handled (i.e. will not be removed if _removalSync is true) 107 * @param id The id of the content 108 */ 109 protected void _handleContent(String id) 110 { 111 _handledContents.add(id); 112 } 113 114 /** 115 * Returns true if the given content is handled 116 * @param id The content to test 117 * @return true if the given content is handled 118 */ 119 protected boolean _isHandled(String id) 120 { 121 return _handledContents.contains(id); 122 } 123 124 /** 125 * Imports or synchronizes a content for each available language 126 * @param idValue The unique identifier of the content 127 * @param remoteValues The remote values 128 * @param forceImport To force import and ignoring the synchronize existing contents only option 129 * @param logger The logger 130 * @return The list of synchronized or imported contents 131 */ 132 protected List<ModifiableContent> _importOrSynchronizeContent(String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 133 { 134 List<ModifiableContent> contents = new ArrayList<>(); 135 136 for (String lang : getLanguages()) 137 { 138 _importOrSynchronizeContent(idValue, lang, remoteValues, forceImport, logger) 139 .ifPresent(contents::add); 140 } 141 142 return contents; 143 } 144 145 /** 146 * Imports or synchronizes a content for a given language 147 * @param idValue The unique identifier of the content 148 * @param lang The language of content to import or synchronize 149 * @param remoteValues The remote values 150 * @param forceImport To force import and ignoring the synchronize existing contents only option 151 * @param logger The logger 152 * @return The imported or synchronized content 153 */ 154 protected Optional<ModifiableContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 155 { 156 try 157 { 158 ModifiableContent content = getContent(lang, idValue); 159 if (content != null) 160 { 161 return Optional.of(_synchronizeContent(content, remoteValues, logger)); 162 } 163 else if (forceImport || !synchronizeExistingContentsOnly()) 164 { 165 return Optional.ofNullable(_importContent(idValue, null, lang, remoteValues, logger)); 166 } 167 } 168 catch (Exception e) 169 { 170 _nbError++; 171 logger.error("An error occurred while importing or synchronizing content", e); 172 } 173 174 return Optional.empty(); 175 } 176 177 @Override 178 public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception 179 { 180 String idValue = content.getValue(getIdField()); 181 182 Map<String, Object> searchParameters = putIdParameter(idValue); 183 Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(searchParameters, logger); 184 if (!results.isEmpty()) 185 { 186 try 187 { 188 _synchronizeContent(content, results.get(idValue), logger); 189 } 190 catch (Exception e) 191 { 192 _nbError++; 193 logger.error("An error occurred while importing or synchronizing content", e); 194 throw e; 195 } 196 } 197 else 198 { 199 logger.warn("The content {} ({}) with synchronization code '{}' doesn't exist anymore in the datasource from SCC '{}'", content.getTitle(), content.getId(), idValue, getId()); 200 } 201 } 202 /** 203 * Synchronize a content with remove values. 204 * @param content The content to synchronize 205 * @param remoteValues Values to synchronize 206 * @param logger The logger 207 * @return The synchronized content 208 * @throws Exception if an error occurs 209 */ 210 protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 211 { 212 long startTime = System.currentTimeMillis(); 213 214 String contentTitle = content.getTitle(); 215 String lang = content.getLanguage(); 216 217 // Update content 218 logger.info("Start synchronizing content '{}' for language {}", contentTitle, lang); 219 220 _sccHelper.updateLastSynchronizationProperties(content); 221 content.saveChanges(); 222 _ensureTitleIsPresent(content, remoteValues, logger); 223 224 boolean hasChanged = _fillContent(remoteValues, content, Map.of(), false, logger); 225 if (hasChanged) 226 { 227 _nbSynchronizedContents++; 228 logger.info("Some changes were detected for content '{}' and language {}", contentTitle, lang); 229 } 230 else 231 { 232 _nbNotChangedContents++; 233 logger.info("No changes detected for content '{}' and language {}", contentTitle, lang); 234 } 235 236 // Do additional operation on the content 237 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 238 if (synchronizingContentOperator != null) 239 { 240 synchronizingContentOperator.additionalOperation(content, remoteValues, logger); 241 } 242 else 243 { 244 logger.warn("Cannot find synchronizing content operator with id '{}'. No additional operation has been done.", getSynchronizingContentOperator()); 245 } 246 long endTime = System.currentTimeMillis(); 247 logger.info("End synchronization of content '{}' for language {} in {} ms", contentTitle, lang, endTime - startTime); 248 249 return content; 250 } 251 252 @Override 253 public List<ModifiableContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception 254 { 255 List<ModifiableContent> createdContents = new ArrayList<>(); 256 257 Map<String, Object> searchParameters = putIdParameter(idValue); 258 Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(searchParameters, logger); 259 if (!results.isEmpty()) 260 { 261 for (String lang : getLanguages()) 262 { 263 if (getContent(lang, idValue) == null) 264 { 265 try 266 { 267 createdContents.add(_importContent(idValue, additionalParameters, lang, results.get(idValue), logger)); 268 } 269 catch (Exception e) 270 { 271 _nbError++; 272 logger.error("An error occurred while importing or synchronizing content", e); 273 } 274 } 275 else if (logger.isWarnEnabled()) 276 { 277 logger.warn("The content of SCC '{}' identified by the synchronization code '{}' and language '{}' already exists, it has not been imported twice.", getId(), idValue, lang); 278 } 279 } 280 } 281 282 return createdContents; 283 } 284 285 /** 286 * Set search parameters for the ID value. 287 * @param idValue Value to search 288 * @return Map with the search parameters 289 */ 290 protected abstract Map<String, Object> putIdParameter(String idValue); 291 292 /** 293 * Import a content from remote values. 294 * @param idValue Id (for import/synchronization) of the content to import 295 * @param additionalParameters Specific parameters for import 296 * @param lang Lang of the content 297 * @param remoteValues Values of the content 298 * @param logger The logger 299 * @return The content created by import, or null 300 * @throws Exception if an error occurs. 301 */ 302 protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 303 { 304 long startTime = System.currentTimeMillis(); 305 306 // Calculate contentTitle 307 String contentTitle = Optional.ofNullable(remoteValues.get(Content.ATTRIBUTE_TITLE)) 308 .map(Collection::stream) 309 .orElseGet(Stream::empty) 310 .filter(String.class::isInstance) 311 .map(String.class::cast) 312 .filter(StringUtils::isNotBlank) 313 .findFirst() 314 .orElse(idValue); 315 316 // Create new content 317 logger.info("Start importing content '{}' for language {}", contentTitle, lang); 318 319 ModifiableContent content = createContentAction(lang, contentTitle, logger); 320 if (content != null) 321 { 322 _sccHelper.updateSCCProperty(content, getId()); 323 _sccHelper.updateLastSynchronizationProperties(content); 324 325 // Force syncCode as soon as possible 326 ValueContext context = ValueContext.newInstance(); 327 if (getLocalAndExternalFields(Map.of("contentTypes", Arrays.asList(content.getTypes()))).contains(getIdField())) 328 { 329 context.withStatus(ExternalizableDataStatus.EXTERNAL); 330 } 331 DataHolderHelper.setValue(content, getIdField(), idValue, context, true); 332 content.saveChanges(); 333 334 // Fill the content with the other values 335 _fillContent(remoteValues, content, additionalParameters, true, logger); 336 337 if (content instanceof WorkflowAwareContent) 338 { 339 // Validate content if allowed 340 validateContent((WorkflowAwareContent) content, logger); 341 } 342 343 _nbCreatedContents++; 344 345 // Do additional operation on the content 346 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 347 synchronizingContentOperator.additionalOperation(content, remoteValues, logger); 348 349 long endTime = System.currentTimeMillis(); 350 logger.info("End import of content '{}' for language {} in {} ms", content.getId(), lang, endTime - startTime); 351 } 352 353 return content; 354 } 355 356 private void _ensureTitleIsPresent(Content content, Map<String, List<Object>> remoteValues, Logger logger) 357 { 358 if (remoteValues.containsKey(Content.ATTRIBUTE_TITLE)) 359 { 360 boolean atLeastOneTitle = remoteValues.get(Content.ATTRIBUTE_TITLE) 361 .stream() 362 .filter(String.class::isInstance) 363 .map(String.class::cast) 364 .anyMatch(StringUtils::isNotBlank); 365 if (atLeastOneTitle) 366 { 367 return; 368 } 369 } 370 371 // Force to current title 372 logger.warn("The remote value of '{}' is empty for the content {}. The '{}' attribute is mandatory, the current title will remain.", Content.ATTRIBUTE_TITLE, content, Content.ATTRIBUTE_TITLE); 373 remoteValues.put(Content.ATTRIBUTE_TITLE, List.of(content.getTitle())); 374 } 375 376 @Override 377 public ModifiableContent getContent(String lang, String idValue) 378 { 379 String query = _getContentPathQuery(lang, idValue, getContentType()); 380 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(query); 381 return contents.stream().findFirst().orElse(null); 382 } 383 384 /** 385 * Creates content action with result from request 386 * @param lang The language 387 * @param contentTitle The content title 388 * @param logger The logger 389 * @return The content id, or null of a workflow error occurs 390 */ 391 protected ModifiableContent createContentAction(String lang, String contentTitle, Logger logger) 392 { 393 return createContentAction(getContentType(), getWorkflowName(), getInitialActionId(), lang, contentTitle, logger); 394 } 395 396 /** 397 * Fill the content with remote values. 398 * @param remoteValues The remote values 399 * @param content The content to synchronize 400 * @param additionalParameters Additional parameters 401 * @param create <code>true</code> if content is creating, false if it is updated 402 * @param logger The logger 403 * @return <code>true</code> if the content has been modified, <code>false</code> otherwise 404 * @throws Exception if an error occurs 405 */ 406 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableContent content, Map<String, Object> additionalParameters, boolean create, Logger logger) throws Exception 407 { 408 if (content instanceof WorkflowAwareContent) 409 { 410 // Transform remote value to get values with the cardinality corresponding to the model 411 Map<String, Object> contentValues = _transformRemoteValuesCardinality(remoteValues, getContentType()); 412 // Remove the id field (if present) because it should be already set before calling this method 413 contentValues.remove(getIdField()); 414 415 // Add additional values 416 contentValues.putAll(getAdditionalAttributeValues(content.getValue(getIdField()), content, additionalParameters, create, logger)); 417 418 // Remove title from values if it is empty 419 if (StringUtils.isEmpty((String) contentValues.get(Content.ATTRIBUTE_TITLE))) 420 { 421 contentValues.remove(Content.ATTRIBUTE_TITLE); 422 } 423 424 Set<String> notSynchronizedContentIds = getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, content.getLanguage(), logger); 425 426 // Get nested values supported by the EditContentFunction 427 Map<String, Object> nestedValues = _getNestedValues(contentValues); 428 return _editContent((WorkflowAwareContent) content, Optional.empty(), nestedValues, additionalParameters, create, notSynchronizedContentIds, logger); 429 } 430 431 return false; 432 } 433 434 /** 435 * Synchronize the content with given values. 436 * @param content The content to synchronize 437 * @param view the view containing the item to edit 438 * @param values the values 439 * @param additionalParameters Additional parameters 440 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 441 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 442 * @param logger The logger 443 * @return <code>true</code> if the content has been modified, <code>false</code> otherwise 444 * @throws WorkflowException if an error occurs 445 */ 446 protected boolean _editContent(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, Map<String, Object> additionalParameters, boolean create, Set<String> notSynchronizedContentIds, Logger logger) throws WorkflowException 447 { 448 SynchronizationContext synchronizationContext = SynchronizationContext.newInstance() 449 .withStatus(ExternalizableDataStatus.EXTERNAL) 450 .withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, getId()) 451 .withIncompatibleValuesIgnored(true); 452 453 if (view.map(v -> content.hasDifferences(v, values, synchronizationContext)) 454 .orElseGet(() -> content.hasDifferences(values, synchronizationContext))) 455 { 456 Map<String, Object> inputs = _getEditInputs(content, view, values, additionalParameters, create, notSynchronizedContentIds, logger); 457 Map<String, Object> actionResult = _contentWorkflowHelper.doAction(content, getSynchronizeActionId(), inputs); 458 return (boolean) actionResult.getOrDefault(AbstractContentWorkflowComponent.HAS_CHANGED_KEY, false); 459 } 460 else 461 { 462 return false; 463 } 464 } 465 466 @SuppressWarnings("unchecked") 467 private static Map<String, Object> _getNestedValues(Map<String, Object> values) 468 { 469 Map<String, Object> nestedValues = new HashMap<>(); 470 for (String key : values.keySet()) 471 { 472 nestedValues = (Map<String, Object>) MapUtils.deepMerge(nestedValues, _getNestedValue(key, values.get(key))); 473 } 474 return nestedValues; 475 } 476 477 @SuppressWarnings("unchecked") 478 private static Map<String, Object> _getNestedValue(String currentPath, Object currentValue) 479 { 480 Map<String, Object> nestedValues = new HashMap<>(); 481 int separatorIndex = currentPath.indexOf('/'); 482 if (separatorIndex < 0) 483 { 484 if (currentValue instanceof Map) 485 { 486 nestedValues.put(currentPath, _getNestedValues((Map<String, Object>) currentValue)); 487 } 488 else 489 { 490 nestedValues.put(currentPath, currentValue); 491 } 492 } 493 else 494 { 495 nestedValues.put(currentPath.substring(0, separatorIndex), _getNestedValue(currentPath.substring(separatorIndex + 1), currentValue)); 496 } 497 return nestedValues; 498 } 499 500 /** 501 * Get the inputs for edit content function. 502 * @param content The content to synchronize 503 * @param view the view containing the item to edit 504 * @param values the values 505 * @param additionalParameters Additional parameters 506 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 507 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 508 * @param logger The logger 509 * @return the input parameters 510 */ 511 protected Map<String, Object> _getEditInputs(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, Map<String, Object> additionalParameters, boolean create, Set<String> notSynchronizedContentIds, Logger logger) 512 { 513 Map<String, Object> inputs = new HashMap<>(); 514 _addEditInputsForSCC(inputs, content, logger); 515 inputs.put(EditSynchronizedContentFunction.ADDITIONAL_PARAMS_KEY, additionalParameters); 516 inputs.put(EditSynchronizedContentFunction.SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, getSynchronizeActionId()); 517 inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds); 518 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, _getEditInputsContextParameters(view, values, create)); 519 return inputs; 520 } 521 522 /** 523 * Add the inputs specific for the SCC to the inputs for edit content function. 524 * @param inputs The inputs to complete 525 * @param content The content to synchronize 526 * @param logger The logger 527 */ 528 protected void _addEditInputsForSCC(Map<String, Object> inputs, WorkflowAwareContent content, Logger logger) 529 { 530 inputs.put(EditSynchronizedContentFunction.SCC_KEY, this); 531 inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, logger); 532 } 533 534 /** 535 * Get the context parameters to add to inputs for edit content function 536 * @param view the view containing the item to edit 537 * @param values the values 538 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 539 * @return the context parameters 540 */ 541 protected Map<String, Object> _getEditInputsContextParameters(Optional<View> view, Map<String, Object> values, boolean create) 542 { 543 Map<String, Object> parameters = new HashMap<>(); 544 parameters.put(EditContentFunction.VALUES_KEY, values); 545 view.ifPresent(v -> parameters.put(EditContentFunction.VIEW, v)); 546 parameters.put(EditContentFunction.QUIT, true); 547 parameters.put(EditSynchronizedContentFunction.IMPORT, create); 548 return parameters; 549 } 550 551 /** 552 * Validates a content after import 553 * @param content The content to validate 554 * @param logger The logger 555 */ 556 protected void validateContent(WorkflowAwareContent content, Logger logger) 557 { 558 if (validateAfterImport()) 559 { 560 validateContent(content, getValidateActionId(), logger); 561 } 562 } 563 564 @Override 565 public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 566 { 567 // Search 568 Map<String, Map<String, Object>> results = internalSearch(_removeEmptyParameters(searchParameters), offset, limit, sort, logger); 569 570 return results; 571 } 572 573 /** 574 * Search values and return the result without any treatment. 575 * @param searchParameters Search parameters to restrict the search 576 * @param offset Begin of the search 577 * @param limit Number of results 578 * @param sort Sort of results (ignored for LDAP results) 579 * @param logger The logger 580 * @return Map of results without any treatment. 581 */ 582 protected abstract Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger); 583 584 /** 585 * Search values and return the result organized by attributes and transformed by the {@link SynchronizingContentOperator} if exists. 586 * @param searchParameters Search parameters to restrict the search 587 * @param logger The logger 588 * @return Map of results organized by attributes. 589 */ 590 protected Map<String, Map<String, List<Object>>> getTransformedRemoteValues(Map<String, Object> searchParameters, Logger logger) 591 { 592 Map<String, Map<String, List<Object>>> remoteValues = getRemoteValues(searchParameters, logger); 593 return _transformRemoteValues(remoteValues, logger); 594 } 595 596 /** 597 * Search values and return the result organized by attributes 598 * @param searchParameters Search parameters to restrict the search 599 * @param logger The logger 600 * @return Map of results organized by attributes. 601 */ 602 protected abstract Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger); 603 604 /** 605 * Transform the given remote values by the {@link SynchronizingContentOperator} if exists. 606 * @param remoteValues The remote values 607 * @param logger The logger 608 * @return the transformed values 609 */ 610 protected Map<String, Map<String, List<Object>>> _transformRemoteValues(Map<String, Map<String, List<Object>>> remoteValues, Logger logger) 611 { 612 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 613 if (synchronizingContentOperator != null) 614 { 615 Map<String, Map<String, List<Object>>> transformedRemoteValues = new LinkedHashMap<>(); 616 ContentType contentType = _contentTypeEP.getExtension(getContentType()); 617 618 for (String key : remoteValues.keySet()) 619 { 620 transformedRemoteValues.put(key, synchronizingContentOperator.transform(contentType, remoteValues.get(key), logger)); 621 } 622 623 return transformedRemoteValues; 624 } 625 else 626 { 627 logger.warn("Cannot find synchronizing content operator with id '{}'. No transformation has applied on remote values", getSynchronizingContentOperator()); 628 return remoteValues; // no transformation 629 } 630 } 631 632 /** 633 * Retrieves additional values to synchronize for a content 634 * @param idValue id value of the content 635 * @param content The content 636 * @param additionalParameters Additional parameters 637 * @param create <code>true</code> if the content has been newly created, <code>false</code> otherwise 638 * @param logger The logger 639 * @return the values to add 640 */ 641 protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger) 642 { 643 // No additional values by default 644 return new LinkedHashMap<>(); 645 } 646 647 /** 648 * Retrieves the ids of the contents related to the given content but that are not part of the synchronization 649 * @param content content 650 * @param contentValues the content values that will be set 651 * @param additionalParameters Additional parameters 652 * @param lang Language of the content 653 * @param logger The logger 654 * @return the ids of the contents that are not part of the synchronization 655 */ 656 protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger) 657 { 658 // All contents are synchronized by default 659 return new HashSet<>(); 660 } 661 662 @Override 663 public void updateSyncInformations(ModifiableContent content, String syncCode, Logger logger) throws Exception 664 { 665 if (StringUtils.isBlank(syncCode)) 666 { 667 _sccHelper.removeSCCProperty(content, getId()); 668 content.removeValue(getIdField()); 669 } 670 else 671 { 672 _sccHelper.updateSCCProperty(content, getId()); 673 content.setValue(getIdField(), syncCode); 674 } 675 676 if (content.needsSave()) 677 { 678 content.saveChanges(); 679 680 if (content instanceof VersionableAmetysObject) 681 { 682 ((VersionableAmetysObject) content).checkpoint(); 683 } 684 } 685 } 686 687 @Override 688 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 689 { 690 return search(searchParameters, 0, Integer.MAX_VALUE, null, logger).size(); 691 } 692 693 /** 694 * Import or synchronize several contents from search params. 695 * @param searchParameters Search parameters 696 * @param forceImport To force import and ignoring the synchronize existing contents only option 697 * @param logger The logger 698 * @return The {@link List} of imported or synchronized {@link ModifiableContent} 699 */ 700 protected final List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger) 701 { 702 return _importOrSynchronizeContents(searchParameters, forceImport, logger, ProgressionTrackerFactory.createContainerProgressionTracker("Import or synchronize contents", logger)); 703 } 704 705 /** 706 * Import or synchronize several contents from search params. 707 * @param searchParameters Search parameters 708 * @param forceImport To force import and ignoring the synchronize existing contents only option 709 * @param logger The logger 710 * @param progressionTracker The progression tracker 711 * @return The {@link List} of imported or synchronized {@link ModifiableContent} 712 */ 713 protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger, ContainerProgressionTracker progressionTracker) 714 { 715 SimpleProgressionTracker progressionTrackerForTransformRemoteValues = progressionTracker.addSimpleStep("transformremotevalues", new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_SCHEDULER_SYNCHRONIZE_COLLECTION_TRANSFORM_STEP_LABEL")); 716 SimpleProgressionTracker progressionTrackerForSynchro = progressionTracker.addSimpleStep("synchronizetransformedremotevalues", new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_SCHEDULER_SYNCHRONIZE_COLLECTION_SYNCHRONIZE_STEP_LABEL")); 717 718 List<ModifiableContent> contents = new ArrayList<>(); 719 720 Map<String, Map<String, List<Object>>> remoteValuesByContent = getTransformedRemoteValues(searchParameters, logger); 721 progressionTrackerForTransformRemoteValues.increment(); 722 723 progressionTrackerForSynchro.setSize(remoteValuesByContent.size()); 724 for (String idValue : remoteValuesByContent.keySet()) 725 { 726 Map<String, List<Object>> remoteValues = remoteValuesByContent.get(idValue); 727 _handleContent(idValue); 728 contents.addAll(_importOrSynchronizeContent(idValue, remoteValues, forceImport, logger)); 729 progressionTrackerForSynchro.increment(); 730 } 731 732 return contents; 733 } 734 735 @Override 736 protected List<Content> _getContentsToRemove(AmetysObjectIterable<ModifiableContent> contents) 737 { 738 return contents.stream() 739 .filter(content -> !_isHandled(content.getValue(getIdField()))) 740 .collect(Collectors.toList()); 741 } 742}