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