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