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 if (create) 410 { 411 // Remove title from values: the title has been set by the create content action 412 contentValues.remove(Content.ATTRIBUTE_TITLE); 413 } 414 415 Set<String> notSynchronizedContentIds = getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, content.getLanguage(), logger); 416 417 // Get nested values supported by the EditContentFunction 418 Map<String, Object> nestedValues = _getNestedValues(contentValues); 419 return _editContent((WorkflowAwareContent) content, Optional.empty(), nestedValues, additionalParameters, create, notSynchronizedContentIds, logger); 420 } 421 422 return false; 423 } 424 425 /** 426 * Synchronize the content with given values. 427 * @param content The content to synchronize 428 * @param view the view containing the item to edit 429 * @param values the values 430 * @param additionalParameters Additional parameters 431 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 432 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 433 * @param logger The logger 434 * @return <code>true</code> if the content has been modified, <code>false</code> otherwise 435 * @throws WorkflowException if an error occurs 436 */ 437 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 438 { 439 Map<String, Object> inputs = _getEditInputs(content, view, values, additionalParameters, create, notSynchronizedContentIds, logger); 440 Map<String, Object> actionResult = _contentWorkflowHelper.doAction(content, getSynchronizeActionId(), inputs); 441 return (boolean) actionResult.getOrDefault(AbstractContentWorkflowComponent.HAS_CHANGED_KEY, false); 442 } 443 444 @SuppressWarnings("unchecked") 445 private static Map<String, Object> _getNestedValues(Map<String, Object> values) 446 { 447 Map<String, Object> nestedValues = new HashMap<>(); 448 for (String key : values.keySet()) 449 { 450 nestedValues = (Map<String, Object>) MapUtils.deepMerge(nestedValues, _getNestedValue(key, values.get(key))); 451 } 452 return nestedValues; 453 } 454 455 @SuppressWarnings("unchecked") 456 private static Map<String, Object> _getNestedValue(String currentPath, Object currentValue) 457 { 458 Map<String, Object> nestedValues = new HashMap<>(); 459 int separatorIndex = currentPath.indexOf('/'); 460 if (separatorIndex < 0) 461 { 462 if (currentValue instanceof Map) 463 { 464 nestedValues.put(currentPath, _getNestedValues((Map<String, Object>) currentValue)); 465 } 466 else 467 { 468 nestedValues.put(currentPath, currentValue); 469 } 470 } 471 else 472 { 473 nestedValues.put(currentPath.substring(0, separatorIndex), _getNestedValue(currentPath.substring(separatorIndex + 1), currentValue)); 474 } 475 return nestedValues; 476 } 477 478 /** 479 * Get the inputs for edit content function. 480 * @param content The content to synchronize 481 * @param view the view containing the item to edit 482 * @param values the values 483 * @param additionalParameters Additional parameters 484 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 485 * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization 486 * @param logger The logger 487 * @return the input parameters 488 */ 489 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) 490 { 491 Map<String, Object> inputs = new HashMap<>(); 492 _addEditInputsForSCC(inputs, content, logger); 493 inputs.put(EditSynchronizedContentFunction.ADDITIONAL_PARAMS_KEY, additionalParameters); 494 inputs.put(EditSynchronizedContentFunction.SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, getSynchronizeActionId()); 495 inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds); 496 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, _getEditInputsContextParameters(view, values, create)); 497 return inputs; 498 } 499 500 /** 501 * Add the inputs specific for the SCC to the inputs for edit content function. 502 * @param inputs The inputs to complete 503 * @param content The content to synchronize 504 * @param logger The logger 505 */ 506 protected void _addEditInputsForSCC(Map<String, Object> inputs, WorkflowAwareContent content, Logger logger) 507 { 508 inputs.put(EditSynchronizedContentFunction.SCC_KEY, this); 509 inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, logger); 510 } 511 512 /** 513 * Get the context parameters to add to inputs for edit content function 514 * @param view the view containing the item to edit 515 * @param values the values 516 * @param create <code>true</code> if content is creating, <code>false</code> if it is updated 517 * @return the context parameters 518 */ 519 protected Map<String, Object> _getEditInputsContextParameters(Optional<View> view, Map<String, Object> values, boolean create) 520 { 521 Map<String, Object> parameters = new HashMap<>(); 522 parameters.put(EditContentFunction.VALUES_KEY, values); 523 view.ifPresent(v -> parameters.put(EditContentFunction.VIEW, v)); 524 parameters.put(EditContentFunction.QUIT, true); 525 parameters.put(EditSynchronizedContentFunction.IMPORT, create); 526 return parameters; 527 } 528 529 /** 530 * Validates a content after import 531 * @param content The content to validate 532 * @param logger The logger 533 */ 534 protected void validateContent(WorkflowAwareContent content, Logger logger) 535 { 536 if (validateAfterImport()) 537 { 538 validateContent(content, getValidateActionId(), logger); 539 } 540 } 541 542 @Override 543 public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 544 { 545 // Search 546 Map<String, Map<String, Object>> results = internalSearch(_removeEmptyParameters(searchParameters), offset, limit, sort, logger); 547 548 return results; 549 } 550 551 /** 552 * Search values and return the result without any treatment. 553 * @param searchParameters Search parameters to restrict the search 554 * @param offset Begin of the search 555 * @param limit Number of results 556 * @param sort Sort of results (ignored for LDAP results) 557 * @param logger The logger 558 * @return Map of results without any treatment. 559 */ 560 protected abstract Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger); 561 562 /** 563 * Search values and return the result organized by attributes and transformed by the {@link SynchronizingContentOperator} if exists. 564 * @param searchParameters Search parameters to restrict the search 565 * @param logger The logger 566 * @return Map of results organized by attributes. 567 */ 568 protected Map<String, Map<String, List<Object>>> getTransformedRemoteValues(Map<String, Object> searchParameters, Logger logger) 569 { 570 Map<String, Map<String, List<Object>>> remoteValues = getRemoteValues(searchParameters, logger); 571 return _transformRemoteValues(remoteValues, logger); 572 } 573 574 /** 575 * Search values and return the result organized by attributes 576 * @param searchParameters Search parameters to restrict the search 577 * @param logger The logger 578 * @return Map of results organized by attributes. 579 */ 580 protected abstract Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger); 581 582 /** 583 * Transform the given remote values by the {@link SynchronizingContentOperator} if exists. 584 * @param remoteValues The remote values 585 * @param logger The logger 586 * @return the transformed values 587 */ 588 protected Map<String, Map<String, List<Object>>> _transformRemoteValues(Map<String, Map<String, List<Object>>> remoteValues, Logger logger) 589 { 590 SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator()); 591 if (synchronizingContentOperator != null) 592 { 593 Map<String, Map<String, List<Object>>> transformedRemoteValues = new LinkedHashMap<>(); 594 ContentType contentType = _contentTypeEP.getExtension(getContentType()); 595 for (String key : remoteValues.keySet()) 596 { 597 transformedRemoteValues.put(key, synchronizingContentOperator.transform(contentType, remoteValues.get(key), logger)); 598 } 599 600 return transformedRemoteValues; 601 } 602 else 603 { 604 logger.warn("Cannot find synchronizing content operator with id '{}'. No transformation has applied on remote values", getSynchronizingContentOperator()); 605 return remoteValues; // no transformation 606 } 607 } 608 609 /** 610 * Retrieves additional values to synchronize for a content 611 * @param idValue id value of the content 612 * @param content The content 613 * @param additionalParameters Additional parameters 614 * @param create <code>true</code> if the content has been newly created, <code>false</code> otherwise 615 * @param logger The logger 616 * @return the values to add 617 */ 618 protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger) 619 { 620 // No additional values by default 621 return new LinkedHashMap<>(); 622 } 623 624 /** 625 * Retrieves the ids of the contents related to the given content but that are not part of the synchronization 626 * @param content content 627 * @param contentValues the content values that will be set 628 * @param additionalParameters Additional parameters 629 * @param lang Language of the content 630 * @param logger The logger 631 * @return the ids of the contents that are not part of the synchronization 632 */ 633 protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger) 634 { 635 // All contents are synchronized by default 636 return new HashSet<>(); 637 } 638 639 @Override 640 public void updateSyncInformations(ModifiableContent content, String syncCode, Logger logger) throws Exception 641 { 642 _sccHelper.updateSCCProperty(content, getId()); 643 content.setValue(getIdField(), syncCode); 644 content.saveChanges(); 645 646 if (content instanceof VersionableAmetysObject) 647 { 648 ((VersionableAmetysObject) content).checkpoint(); 649 } 650 } 651 652 @Override 653 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 654 { 655 return search(searchParameters, 0, Integer.MAX_VALUE, null, logger).size(); 656 } 657 658 /** 659 * Import or synchronize several contents from search params. 660 * @param searchParameters Search parameters 661 * @param forceImport To force import and ignoring the synchronize existing contents only option 662 * @param logger The logger 663 * @return The {@link List} of imported or synchronized {@link ModifiableContent} 664 */ 665 protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger) 666 { 667 List<ModifiableContent> contents = new ArrayList<>(); 668 669 Map<String, Map<String, List<Object>>> remoteValuesByContent = getTransformedRemoteValues(searchParameters, logger); 670 for (String idValue : remoteValuesByContent.keySet()) 671 { 672 Map<String, List<Object>> remoteValues = remoteValuesByContent.get(idValue); 673 _handleContent(idValue); 674 contents.addAll(_importOrSynchronizeContent(idValue, remoteValues, forceImport, logger)); 675 } 676 677 return contents; 678 } 679 680 @Override 681 protected List<Content> _getContentsToRemove(AmetysObjectIterable<ModifiableContent> contents) 682 { 683 return contents.stream() 684 .filter(content -> !_isHandled(content.getValue(getIdField()))) 685 .collect(Collectors.toList()); 686 } 687}