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 try 264 { 265 createdContents.add(_importContent(idValue, additionalParameters, lang, results.get(idValue), logger)); 266 } 267 catch (Exception e) 268 { 269 _nbError++; 270 logger.error("An error occurred while importing or synchronizing content", e); 271 } 272 } 273 } 274 275 return createdContents; 276 } 277 278 /** 279 * Set search parameters for the ID value. 280 * @param idValue Value to search 281 * @return Map with the search parameters 282 */ 283 protected abstract Map<String, Object> putIdParameter(String idValue); 284 285 /** 286 * Import a content from remote values. 287 * @param idValue Id (for import/synchronization) of the content to import 288 * @param additionalParameters Specific parameters for import 289 * @param lang Lang of the content 290 * @param remoteValues Values of the content 291 * @param logger The logger 292 * @return The content created by import, or null 293 * @throws Exception if an error occurs. 294 */ 295 protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 296 { 297 long startTime = System.currentTimeMillis(); 298 299 // Calculate contentTitle 300 String contentTitle = Optional.ofNullable(remoteValues.get(Content.ATTRIBUTE_TITLE)) 301 .map(Collection::stream) 302 .orElseGet(Stream::empty) 303 .filter(String.class::isInstance) 304 .map(String.class::cast) 305 .filter(StringUtils::isNotBlank) 306 .findFirst() 307 .orElse(idValue); 308 309 // Create new content 310 logger.info("Start importing content '{}' for language {}", contentTitle, lang); 311 312 ModifiableContent content = createContentAction(lang, contentTitle, logger); 313 if (content != null) 314 { 315 _sccHelper.updateSCCProperty(content, getId()); 316 _sccHelper.updateLastSynchronizationProperties(content); 317 318 // Force syncCode as soon as possible 319 String syncCode = remoteValues.get(getIdField()).get(0).toString(); 320 ValueContext context = ValueContext.newInstance(); 321 if (getLocalAndExternalFields(Map.of("contentTypes", Arrays.asList(content.getTypes()))).contains(getIdField())) 322 { 323 context.withStatus(ExternalizableDataStatus.EXTERNAL); 324 } 325 DataHolderHelper.setValue(content, getIdField(), syncCode, context, true); 326 content.saveChanges(); 327 328 _fillContent(remoteValues, content, additionalParameters, true, logger); 329 330 if (content instanceof WorkflowAwareContent) 331 { 332 // Validate content if allowed 333 validateContent((WorkflowAwareContent) content, logger); 334 } 335 336 _nbCreatedContents++; 337 338 // Do additional operation on the content 339 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 340 synchronizingContentOperator.additionalOperation(content, remoteValues, logger); 341 342 long endTime = System.currentTimeMillis(); 343 logger.info("End import of content '{}' for language {} in {} ms", content.getId(), lang, endTime - startTime); 344 } 345 346 return content; 347 } 348 349 private void _ensureTitleIsPresent(Content content, Map<String, List<Object>> remoteValues, Logger logger) 350 { 351 if (remoteValues.containsKey(Content.ATTRIBUTE_TITLE)) 352 { 353 boolean atLeastOneTitle = remoteValues.get(Content.ATTRIBUTE_TITLE) 354 .stream() 355 .filter(String.class::isInstance) 356 .map(String.class::cast) 357 .anyMatch(StringUtils::isNotBlank); 358 if (atLeastOneTitle) 359 { 360 return; 361 } 362 } 363 364 // Force to current title 365 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); 366 remoteValues.put(Content.ATTRIBUTE_TITLE, List.of(content.getTitle())); 367 } 368 369 @Override 370 public ModifiableContent getContent(String lang, String idValue) 371 { 372 String query = _getContentPathQuery(lang, idValue, getContentType()); 373 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(query); 374 375 if (contents.getSize() > 0) 376 { 377 return contents.iterator().next(); 378 } 379 return null; 380 } 381 382 /** 383 * Creates content action with result from request 384 * @param lang The language 385 * @param contentTitle The content title 386 * @param logger The logger 387 * @return The content id, or null of a workflow error occurs 388 */ 389 protected ModifiableContent createContentAction(String lang, String contentTitle, Logger logger) 390 { 391 return createContentAction(getContentType(), getWorkflowName(), getInitialActionId(), lang, contentTitle, logger); 392 } 393 394 /** 395 * Fill the content with remote values. 396 * @param remoteValues The remote values 397 * @param content The content to synchronize 398 * @param additionalParameters Additional parameters 399 * @param create <code>true</code> if content is creating, false if it is updated 400 * @param logger The logger 401 * @return <code>true</code> if the content has been modified, <code>false</code> otherwise 402 * @throws Exception if an error occurs 403 */ 404 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableContent content, Map<String, Object> additionalParameters, boolean create, Logger logger) throws Exception 405 { 406 if (content instanceof WorkflowAwareContent) 407 { 408 // Transform remote value to get values with the cardinality corresponding to the model 409 Map<String, Object> contentValues = _transformRemoteValuesCardinality(remoteValues, getContentType()); 410 // Remove the id field because it should be already set before calling this method 411 String idValue = (String) contentValues.remove(getIdField()); 412 413 // Add additional values 414 contentValues.putAll(getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger)); 415 416 // Remove title from values if it is empty 417 if (StringUtils.isEmpty((String) contentValues.get(Content.ATTRIBUTE_TITLE))) 418 { 419 contentValues.remove(Content.ATTRIBUTE_TITLE); 420 } 421 422 Set<String> notSynchronizedContentIds = getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, content.getLanguage(), logger); 423 424 // Get nested values supported by the EditContentFunction 425 Map<String, Object> nestedValues = _getNestedValues(contentValues); 426 return _editContent((WorkflowAwareContent) content, Optional.empty(), nestedValues, additionalParameters, create, notSynchronizedContentIds, logger); 427 } 428 429 return false; 430 } 431 432 /** 433 * Synchronize the content with given values. 434 * @param content The content to synchronize 435 * @param view the view containing the item to edit 436 * @param values the values 437 * @param additionalParameters Additional parameters 438 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 439 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 440 * @param logger The logger 441 * @return <code>true</code> if the content has been modified, <code>false</code> otherwise 442 * @throws WorkflowException if an error occurs 443 */ 444 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 445 { 446 SynchronizationContext synchronizationContext = SynchronizationContext.newInstance() 447 .withStatus(ExternalizableDataStatus.EXTERNAL) 448 .withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, getId()) 449 .withIncompatibleValuesIgnored(true); 450 451 if (view.map(v -> content.hasDifferences(v, values, synchronizationContext)) 452 .orElseGet(() -> content.hasDifferences(values, synchronizationContext))) 453 { 454 Map<String, Object> inputs = _getEditInputs(content, view, values, additionalParameters, create, notSynchronizedContentIds, logger); 455 Map<String, Object> actionResult = _contentWorkflowHelper.doAction(content, getSynchronizeActionId(), inputs); 456 return (boolean) actionResult.getOrDefault(AbstractContentWorkflowComponent.HAS_CHANGED_KEY, false); 457 } 458 else 459 { 460 return false; 461 } 462 } 463 464 @SuppressWarnings("unchecked") 465 private static Map<String, Object> _getNestedValues(Map<String, Object> values) 466 { 467 Map<String, Object> nestedValues = new HashMap<>(); 468 for (String key : values.keySet()) 469 { 470 nestedValues = (Map<String, Object>) MapUtils.deepMerge(nestedValues, _getNestedValue(key, values.get(key))); 471 } 472 return nestedValues; 473 } 474 475 @SuppressWarnings("unchecked") 476 private static Map<String, Object> _getNestedValue(String currentPath, Object currentValue) 477 { 478 Map<String, Object> nestedValues = new HashMap<>(); 479 int separatorIndex = currentPath.indexOf('/'); 480 if (separatorIndex < 0) 481 { 482 if (currentValue instanceof Map) 483 { 484 nestedValues.put(currentPath, _getNestedValues((Map<String, Object>) currentValue)); 485 } 486 else 487 { 488 nestedValues.put(currentPath, currentValue); 489 } 490 } 491 else 492 { 493 nestedValues.put(currentPath.substring(0, separatorIndex), _getNestedValue(currentPath.substring(separatorIndex + 1), currentValue)); 494 } 495 return nestedValues; 496 } 497 498 /** 499 * Get the inputs for edit content function. 500 * @param content The content to synchronize 501 * @param view the view containing the item to edit 502 * @param values the values 503 * @param additionalParameters Additional parameters 504 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 505 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 506 * @param logger The logger 507 * @return the input parameters 508 */ 509 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) 510 { 511 Map<String, Object> inputs = new HashMap<>(); 512 _addEditInputsForSCC(inputs, content, logger); 513 inputs.put(EditSynchronizedContentFunction.ADDITIONAL_PARAMS_KEY, additionalParameters); 514 inputs.put(EditSynchronizedContentFunction.SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, getSynchronizeActionId()); 515 inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds); 516 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, _getEditInputsContextParameters(view, values, create)); 517 return inputs; 518 } 519 520 /** 521 * Add the inputs specific for the SCC to the inputs for edit content function. 522 * @param inputs The inputs to complete 523 * @param content The content to synchronize 524 * @param logger The logger 525 */ 526 protected void _addEditInputsForSCC(Map<String, Object> inputs, WorkflowAwareContent content, Logger logger) 527 { 528 inputs.put(EditSynchronizedContentFunction.SCC_KEY, this); 529 inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, logger); 530 } 531 532 /** 533 * Get the context parameters to add to inputs for edit content function 534 * @param view the view containing the item to edit 535 * @param values the values 536 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 537 * @return the context parameters 538 */ 539 protected Map<String, Object> _getEditInputsContextParameters(Optional<View> view, Map<String, Object> values, boolean create) 540 { 541 Map<String, Object> parameters = new HashMap<>(); 542 parameters.put(EditContentFunction.VALUES_KEY, values); 543 view.ifPresent(v -> parameters.put(EditContentFunction.VIEW, v)); 544 parameters.put(EditContentFunction.QUIT, true); 545 parameters.put(EditSynchronizedContentFunction.IMPORT, create); 546 return parameters; 547 } 548 549 /** 550 * Validates a content after import 551 * @param content The content to validate 552 * @param logger The logger 553 */ 554 protected void validateContent(WorkflowAwareContent content, Logger logger) 555 { 556 if (validateAfterImport()) 557 { 558 validateContent(content, getValidateActionId(), logger); 559 } 560 } 561 562 @Override 563 public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 564 { 565 // Search 566 Map<String, Map<String, Object>> results = internalSearch(_removeEmptyParameters(searchParameters), offset, limit, sort, logger); 567 568 return results; 569 } 570 571 /** 572 * Search values and return the result without any treatment. 573 * @param searchParameters Search parameters to restrict the search 574 * @param offset Begin of the search 575 * @param limit Number of results 576 * @param sort Sort of results (ignored for LDAP results) 577 * @param logger The logger 578 * @return Map of results without any treatment. 579 */ 580 protected abstract Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger); 581 582 /** 583 * Search values and return the result organized by attributes and transformed by the {@link SynchronizingContentOperator} if exists. 584 * @param searchParameters Search parameters to restrict the search 585 * @param logger The logger 586 * @return Map of results organized by attributes. 587 */ 588 protected Map<String, Map<String, List<Object>>> getTransformedRemoteValues(Map<String, Object> searchParameters, Logger logger) 589 { 590 Map<String, Map<String, List<Object>>> remoteValues = getRemoteValues(searchParameters, logger); 591 return _transformRemoteValues(remoteValues, logger); 592 } 593 594 /** 595 * Search values and return the result organized by attributes 596 * @param searchParameters Search parameters to restrict the search 597 * @param logger The logger 598 * @return Map of results organized by attributes. 599 */ 600 protected abstract Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger); 601 602 /** 603 * Transform the given remote values by the {@link SynchronizingContentOperator} if exists. 604 * @param remoteValues The remote values 605 * @param logger The logger 606 * @return the transformed values 607 */ 608 protected Map<String, Map<String, List<Object>>> _transformRemoteValues(Map<String, Map<String, List<Object>>> remoteValues, Logger logger) 609 { 610 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 611 if (synchronizingContentOperator != null) 612 { 613 Map<String, Map<String, List<Object>>> transformedRemoteValues = new LinkedHashMap<>(); 614 ContentType contentType = _contentTypeEP.getExtension(getContentType()); 615 616 for (String key : remoteValues.keySet()) 617 { 618 transformedRemoteValues.put(key, synchronizingContentOperator.transform(contentType, remoteValues.get(key), logger)); 619 } 620 621 return transformedRemoteValues; 622 } 623 else 624 { 625 logger.warn("Cannot find synchronizing content operator with id '{}'. No transformation has applied on remote values", getSynchronizingContentOperator()); 626 return remoteValues; // no transformation 627 } 628 } 629 630 /** 631 * Retrieves additional values to synchronize for a content 632 * @param idValue id value of the content 633 * @param content The content 634 * @param additionalParameters Additional parameters 635 * @param create <code>true</code> if the content has been newly created, <code>false</code> otherwise 636 * @param logger The logger 637 * @return the values to add 638 */ 639 protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger) 640 { 641 // No additional values by default 642 return new LinkedHashMap<>(); 643 } 644 645 /** 646 * Retrieves the ids of the contents related to the given content but that are not part of the synchronization 647 * @param content content 648 * @param contentValues the content values that will be set 649 * @param additionalParameters Additional parameters 650 * @param lang Language of the content 651 * @param logger The logger 652 * @return the ids of the contents that are not part of the synchronization 653 */ 654 protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger) 655 { 656 // All contents are synchronized by default 657 return new HashSet<>(); 658 } 659 660 @Override 661 public void updateSyncInformations(ModifiableContent content, String syncCode, Logger logger) throws Exception 662 { 663 _sccHelper.updateSCCProperty(content, getId()); 664 content.setValue(getIdField(), syncCode); 665 content.saveChanges(); 666 667 if (content instanceof VersionableAmetysObject) 668 { 669 ((VersionableAmetysObject) content).checkpoint(); 670 } 671 } 672 673 @Override 674 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 675 { 676 return search(searchParameters, 0, Integer.MAX_VALUE, null, logger).size(); 677 } 678 679 /** 680 * Import or synchronize several contents from search params. 681 * @param searchParameters Search parameters 682 * @param forceImport To force import and ignoring the synchronize existing contents only option 683 * @param logger The logger 684 * @return The {@link List} of imported or synchronized {@link ModifiableContent} 685 */ 686 protected final List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger) 687 { 688 return _importOrSynchronizeContents(searchParameters, forceImport, logger, ProgressionTrackerFactory.createContainerProgressionTracker("Import or synchronize contents", logger)); 689 } 690 691 /** 692 * Import or synchronize several contents from search params. 693 * @param searchParameters Search parameters 694 * @param forceImport To force import and ignoring the synchronize existing contents only option 695 * @param logger The logger 696 * @param progressionTracker The progression tracker 697 * @return The {@link List} of imported or synchronized {@link ModifiableContent} 698 */ 699 protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger, ContainerProgressionTracker progressionTracker) 700 { 701 SimpleProgressionTracker progressionTrackerForTransformRemoteValues = progressionTracker.addSimpleStep("transformremotevalues", new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_SCHEDULER_SYNCHRONIZE_COLLECTION_TRANSFORM_STEP_LABEL")); 702 SimpleProgressionTracker progressionTrackerForSynchro = progressionTracker.addSimpleStep("synchronizetransformedremotevalues", new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_SCHEDULER_SYNCHRONIZE_COLLECTION_SYNCHRONIZE_STEP_LABEL")); 703 704 List<ModifiableContent> contents = new ArrayList<>(); 705 706 Map<String, Map<String, List<Object>>> remoteValuesByContent = getTransformedRemoteValues(searchParameters, logger); 707 progressionTrackerForTransformRemoteValues.increment(); 708 709 progressionTrackerForSynchro.setSize(remoteValuesByContent.size()); 710 for (String idValue : remoteValuesByContent.keySet()) 711 { 712 Map<String, List<Object>> remoteValues = remoteValuesByContent.get(idValue); 713 _handleContent(idValue); 714 contents.addAll(_importOrSynchronizeContent(idValue, remoteValues, forceImport, logger)); 715 progressionTrackerForSynchro.increment(); 716 } 717 718 return contents; 719 } 720 721 @Override 722 protected List<Content> _getContentsToRemove(AmetysObjectIterable<ModifiableContent> contents) 723 { 724 return contents.stream() 725 .filter(content -> !_isHandled(content.getValue(getIdField()))) 726 .collect(Collectors.toList()); 727 } 728}