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