001/* 002 * Copyright 2019 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.cms.data; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.function.Function; 029import java.util.stream.Collectors; 030import java.util.stream.IntStream; 031import java.util.stream.Stream; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.collections4.SetUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.tuple.ImmutablePair; 040import org.apache.commons.lang3.tuple.Pair; 041 042import org.ametys.cms.contenttype.ContentAttributeDefinition; 043import org.ametys.cms.contenttype.ContentType; 044import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 045import org.ametys.cms.data.type.ModelItemTypeConstants; 046import org.ametys.cms.repository.Content; 047import org.ametys.cms.repository.ModifiableContent; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 050import org.ametys.plugins.repository.data.holder.DataHolder; 051import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 052import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 053import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater; 054import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry; 055import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareComposite; 056import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater; 057import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry; 058import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 059import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 060import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 061import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 062import org.ametys.plugins.repository.data.holder.values.ValueContext; 063import org.ametys.plugins.repository.model.RepeaterDefinition; 064import org.ametys.plugins.repository.model.ViewHelper; 065import org.ametys.runtime.model.ModelItem; 066import org.ametys.runtime.model.ModelViewItemGroup; 067import org.ametys.runtime.model.View; 068import org.ametys.runtime.model.ViewItemContainer; 069import org.ametys.runtime.model.exception.BadItemTypeException; 070 071/** 072 * Helper for data of type 'content' 073 */ 074public class ContentDataHelper implements Serviceable, Component 075{ 076 /** Avalon role */ 077 public static final String ROLE = ContentDataHelper.class.getName(); 078 079 private AmetysObjectResolver _resolver; 080 private ContentTypeExtensionPoint _cTypeExtensionPoint; 081 082 public void service(ServiceManager manager) throws ServiceException 083 { 084 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 085 _cTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 086 } 087 088 /** 089 * Retrieves the content identifier of a content data 090 * @param dataHolder data holder that contains the content data 091 * @param dataPath path to the content data 092 * @param defaultValue The default value to return 093 * @return the content identifier 094 * @throws BadItemTypeException if the data at the given path is not a content data 095 */ 096 public static String getContentIdFromContentData(ModelAwareDataHolder dataHolder, String dataPath, String defaultValue) throws BadItemTypeException 097 { 098 ContentValue value = dataHolder.getValue(dataPath); 099 return Optional.ofNullable(value) 100 .map(ContentValue::getContentId) 101 .orElse(defaultValue); 102 } 103 104 /** 105 * Retrieves the content identifier of a content data 106 * @param dataHolder data holder that contains the content data 107 * @param dataPath path to the content data 108 * @return the content identifier, empty string if it is invalid 109 * @throws BadItemTypeException if the data at the given path is not a content data 110 */ 111 public static String getContentIdFromContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException 112 { 113 return getContentIdFromContentData(dataHolder, dataPath, StringUtils.EMPTY); 114 } 115 116 /** 117 * Retrieves the content identifiers in an array from a multiple content data 118 * @param dataHolder data holder that contains the multiple content data 119 * @param dataPath path to the multiple content data 120 * @return an array containing the content identifiers 121 * @throws BadItemTypeException if the data at the given path is not a content data 122 */ 123 public static boolean isMultipleContentDataEmpty(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException 124 { 125 return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath).count() <= 0; 126 } 127 128 /** 129 * Retrieves the content identifiers in a {@link List} from a multiple content data 130 * @param dataHolder data holder that contains the multiple content data 131 * @param dataPath path to the multiple content data 132 * @return a {@link List} containing the content identifiers 133 * @throws BadItemTypeException if the data at the given path is not a content data 134 */ 135 public static List<String> getContentIdsListFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException 136 { 137 return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath).collect(Collectors.toList()); 138 } 139 140 /** 141 * Retrieves the content identifiers in an array from a multiple content data 142 * @param dataHolder data holder that contains the multiple content data 143 * @param dataPath path to the multiple content data 144 * @return an array containing the content identifiers 145 * @throws BadItemTypeException if the data at the given path is not a content data 146 */ 147 public static String[] getContentIdsArrayFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException 148 { 149 return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath).toArray(String[]::new); 150 } 151 152 /** 153 * Retrieves a {@link Stream} of the content identifiers from a multiple content data 154 * @param dataHolder data holder that contains the multiple content data 155 * @param dataPath path to the multiple content data 156 * @return a {@link Stream} of the content identifiers 157 * @throws BadItemTypeException if the data at the given path is not a content data 158 */ 159 public static Stream<String> getContentIdsStreamFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException 160 { 161 ContentValue[] value = dataHolder.getValue(dataPath, false, new ContentValue[0]); 162 return Optional.ofNullable(value) 163 .map(v -> Arrays.stream(v)) 164 .orElse(Stream.empty()) 165 .map(ContentValue::getContentId); 166 } 167 168 /** 169 * Prepares a write operation in the given {@link ModelAwareDataHolder} by traversing a {@link View} with the values to be written.<br> 170 * The goal is to find content attributes and to extract added and removed values. 171 * @param viewItemContainer the view to walk through 172 * @param content the source {@link ModifiableContent} 173 * @param values the new values 174 * @return the contents to-be-added and to-be-removed 175 */ 176 public Collection<ReferencedContents> collectReferencedContents(ViewItemContainer viewItemContainer, ModifiableContent content, Map<String, Object> values) 177 { 178 // content ids, grouped by invert attribute paths, instead of definition paths, so that if several attributes share the same invert attribute, they are handled together 179 // data structure is <definition> mapped <previous contents mapped with their data paths, new contents mapped with their data paths> 180 Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references = new HashMap<>(); 181 182 _collectReferencedContents(viewItemContainer, Optional.of(content), "", Optional.of(values), false, references); 183 184 Collection<ReferencedContents> contents = new ArrayList<>(); 185 for (ContentAttributeDefinition definition : references.keySet()) 186 { 187 Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>> allValues = references.get(definition); 188 Map<ContentValue, List<String>> previousValues = allValues.getLeft(); 189 Map<ContentValue, List<String>> newValues = allValues.getRight(); 190 191 Map<ContentValue, List<String>> addedValues = new HashMap<>(); 192 Map<ContentValue, List<String>> removedValues = new HashMap<>(); 193 194 newValues.forEach((value, list) -> { 195 if (!previousValues.containsKey(value)) 196 { 197 addedValues.put(value, list); 198 } 199 }); 200 201 previousValues.forEach((value, list) -> { 202 if (!newValues.containsKey(value)) 203 { 204 removedValues.put(value, list); 205 } 206 }); 207 208 String invert = definition.getInvertRelationPath(); 209 ContentType invertType = _cTypeExtensionPoint.getExtension(definition.getContentTypeId()); 210 ContentAttributeDefinition invertDefinition = (ContentAttributeDefinition) invertType.getModelItem(invert); 211 212 Map<ContentValue, ContentValue> thirdPartyContents = new HashMap<>(); 213 if (!DataHolderHelper.isMultiple(invertDefinition)) 214 { 215 List<ModifiableContent> refContents = addedValues.keySet().stream().map(v -> v.getContentIfExists()).flatMap(Optional::stream).collect(Collectors.toList()); 216 217 for (ModifiableContent refContent : refContents) 218 { 219 ContentValue previousValue = refContent.getValue(invert); 220 if (previousValue != null) 221 { 222 String previousContentId = previousValue.getContentId(); 223 if (!content.getId().equals(previousContentId)) 224 { 225 // the single-valued content attribute is about to change, we should store the 3rd party content involved in the invert relation 226 thirdPartyContents.put(new ContentValue(refContent), previousValue); 227 } 228 } 229 } 230 } 231 232 contents.add(new ReferencedContents(definition, addedValues, removedValues, thirdPartyContents)); 233 } 234 235 return contents; 236 } 237 238 private Set<ContentValue> _getContentRefs(Object value) 239 { 240 if (value instanceof SynchronizableValue) 241 { 242 SynchronizableValue syncValue = (SynchronizableValue) value; 243 ExternalizableDataStatus status = syncValue.getExternalizableStatus(); 244 245 Object realValue; 246 if (status == null || status == ExternalizableDataStatus.LOCAL) 247 { 248 realValue = syncValue.getValue(); 249 } 250 else 251 { 252 realValue = syncValue.getExternalValue(); 253 } 254 255 return _getContentRefs(realValue); 256 } 257 else if (value instanceof ContentValue) 258 { 259 return Set.of((ContentValue) value); 260 } 261 else if (value instanceof ContentValue[]) 262 { 263 return Set.copyOf(Arrays.asList((ContentValue[]) value)); 264 } 265 else if (value instanceof String) 266 { 267 return Set.of(new ContentValue(_resolver, (String) value)); 268 } 269 else if (value instanceof String[]) 270 { 271 return Arrays.stream((String[]) value).map(id -> new ContentValue(_resolver, id)).collect(Collectors.toSet()); 272 } 273 else if (value instanceof ModifiableContent) 274 { 275 return Set.of(new ContentValue((ModifiableContent) value)); 276 } 277 else if (value instanceof ModifiableContent[]) 278 { 279 return Arrays.stream((ModifiableContent[]) value).map(ContentValue::new).collect(Collectors.toSet()); 280 } 281 282 return Collections.emptySet(); 283 } 284 285 private void _addReferences(Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references, ContentAttributeDefinition definition, String dataPath, Set<ContentValue> previousContents, Set<ContentValue> newContents) 286 { 287 Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>> values = references.get(definition); 288 Map<ContentValue, List<String>> previousValues = values == null ? new HashMap<>() : values.getLeft(); 289 Map<ContentValue, List<String>> newValues = values == null ? new HashMap<>() : values.getRight(); 290 291 previousContents.forEach(c -> previousValues.computeIfAbsent(c, __ -> new ArrayList<>()).add(dataPath)); 292 newContents.forEach(c -> newValues.computeIfAbsent(c, __ -> new ArrayList<>()).add(dataPath)); 293 294 references.put(definition, Pair.of(previousValues, newValues)); 295 } 296 297 @SuppressWarnings("unchecked") 298 private void _collectReferencedContents(ViewItemContainer viewItemContainer, 299 Optional<ModelAwareDataHolder> currentDataHolder, 300 String dataPathPrefix, 301 Optional<Map<String, Object>> values, 302 boolean insideKeptDataHolder, 303 Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references) 304 { 305 ViewHelper.visitView(viewItemContainer, 306 (element, definition) -> { 307 // simple element 308 String name = definition.getName(); 309 310 if (definition instanceof ContentAttributeDefinition) 311 { 312 ContentAttributeDefinition contentDefinition = (ContentAttributeDefinition) definition; 313 String invert = contentDefinition.getInvertRelationPath(); 314 if (invert != null) 315 { 316 _collectInvertRelation(currentDataHolder, dataPathPrefix, values, insideKeptDataHolder, name, contentDefinition, references); 317 } 318 } 319 }, 320 (group, definition) -> { 321 // composite 322 String name = definition.getName(); 323 _collectReferencedContents(group, currentDataHolder.map(v -> v.getComposite(name)), dataPathPrefix + name + "/", values.map(v -> (Map<String, Object>) v.get(name)), insideKeptDataHolder, references); 324 }, 325 (group, definition) -> { 326 // repeater 327 String name = definition.getName(); 328 _collectRepeater(group, currentDataHolder, dataPathPrefix, values, insideKeptDataHolder, name, references); 329 }, 330 group -> _collectReferencedContents(group, currentDataHolder, dataPathPrefix, values, insideKeptDataHolder, references)); 331 } 332 333 private void _collectInvertRelation(Optional<ModelAwareDataHolder> currentDataHolder, 334 String dataPathPrefix, 335 Optional<Map<String, Object>> values, 336 boolean insideKeptDataHolder, 337 String name, 338 ContentAttributeDefinition definition, 339 Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references) 340 { 341 String dataPath = dataPathPrefix + name; 342 343 Set<ContentValue> previousContents = currentDataHolder 344 .map(dh -> dh.getValue(name)) 345 .map(this::_getContentRefs) 346 .orElseGet(Collections::emptySet); 347 Set<ContentValue> newContents; 348 349 if (insideKeptDataHolder) 350 { 351 newContents = previousContents; 352 } 353 else 354 { 355 Optional<Object> rawValue = values.map(v -> v.get(name)); 356 357 // If the current value is an untouched value, don't edit the invert relation 358 if (rawValue.filter(UntouchedValue.class::isInstance).isPresent()) 359 { 360 return; 361 } 362 363 newContents = rawValue 364 .map(this::_getContentRefs) 365 .orElseGet(Collections::emptySet); 366 367 SynchronizableValue.Mode mode = values.map(v -> v.get(name)) 368 .filter(SynchronizableValue.class::isInstance) 369 .map(SynchronizableValue.class::cast) 370 .map(v -> v.getMode()) 371 .orElse(SynchronizableValue.Mode.REPLACE); 372 373 if (mode == SynchronizableValue.Mode.REMOVE) 374 { 375 newContents = SetUtils.difference(previousContents, newContents); 376 } 377 else if (mode == SynchronizableValue.Mode.APPEND) 378 { 379 newContents = SetUtils.union(previousContents, newContents); 380 } 381 } 382 383 _addReferences(references, definition, dataPath, previousContents, newContents); 384 } 385 386 @SuppressWarnings("unchecked") 387 private void _collectRepeater(ModelViewItemGroup group, 388 Optional<ModelAwareDataHolder> currentDataHolder, 389 String dataPathPrefix, 390 Optional<Map<String, Object>> values, 391 boolean insideKeptDataHolder, 392 String name, 393 Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references) 394 { 395 Object value = values.map(v -> v.get(name)).orElse(null); 396 List<Map<String, Object>> newEntries = null; 397 Map<Integer, Integer> mapping = null; 398 SynchronizableRepeater.Mode mode = SynchronizableRepeater.Mode.REPLACE_ALL; 399 if (value instanceof List) 400 { 401 newEntries = (List<Map<String, Object>>) value; 402 mapping = IntStream.rangeClosed(1, newEntries.size()).boxed().collect(Collectors.toMap(Function.identity(), Function.identity())); 403 } 404 else if (value instanceof SynchronizableRepeater) 405 { 406 newEntries = ((SynchronizableRepeater) value).getEntries(); 407 mapping = ((SynchronizableRepeater) value).getPositionsMapping(); 408 mode = ((SynchronizableRepeater) value).getMode(); 409 } 410 411 // first collect data for actually existing entries 412 Set<Integer> alreadyHandledValues = new HashSet<>(); 413 ModelAwareRepeater repeater = currentDataHolder.map(v -> v.getRepeater(name)).orElse(null); 414 if (repeater != null) 415 { 416 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 417 { 418 int position = entry.getPosition(); 419 420 Optional<Map<String, Object>> newValues = Optional.empty(); 421 String newPositionSuffix = ""; 422 boolean newKeptValue = insideKeptDataHolder; 423 424 if (!insideKeptDataHolder) 425 { 426 if (mode == SynchronizableRepeater.Mode.REPLACE_ALL) 427 { 428 Integer newPosition = mapping != null ? mapping.get(position) : null; 429 430 if (newPosition != null) 431 { 432 // not removed entry 433 newValues = newEntries != null ? Optional.ofNullable(newEntries.get(newPosition - 1)) : Optional.empty(); 434 newPositionSuffix = "[" + newPosition + "]"; 435 alreadyHandledValues.add(newPosition); 436 } 437 } 438 else if (mode == SynchronizableRepeater.Mode.REPLACE) 439 { 440 List<Integer> replacePositions = ((SynchronizableRepeater) value).getReplacePositions(); 441 int index = replacePositions.indexOf(position); 442 443 if (index >= 0) 444 { 445 assert newEntries != null; 446 newValues = Optional.ofNullable(newEntries.get(index)); 447 newPositionSuffix = "[" + position + "]"; 448 alreadyHandledValues.add(position); 449 } 450 else 451 { 452 // entry kept as is 453 newKeptValue = true; 454 } 455 } 456 else 457 { 458 Set<Integer> removedEntries = ((SynchronizableRepeater) value).getRemovedEntries(); 459 newKeptValue = !removedEntries.contains(position); 460 } 461 } 462 463 _collectReferencedContents(group, Optional.of(entry), dataPathPrefix + name + newPositionSuffix + "/", newValues, newKeptValue, references); 464 } 465 } 466 467 // then collect data for newly created entries 468 if (newEntries != null) 469 { 470 for (int i = 1; i <= newEntries.size(); i++) 471 { 472 if (!alreadyHandledValues.contains(i)) 473 { 474 _collectReferencedContents(group, Optional.empty(), dataPathPrefix + name + "[" + i + "]/", Optional.of(newEntries.get(i - 1)), insideKeptDataHolder, references); 475 } 476 } 477 } 478 } 479 480 /** 481 * Find any broken reference in data holder's content attributes. 482 * A reference is considered as broken if the referenced content does not exist 483 * @param dataHolder the {@link ModelAwareDataHolder} to inspect. 484 * @param fix if <code>true</code>, and the dataHolder is modifiable, broken references will be removed 485 * Warning: if modifications are made on the given data holder, the modifications won't be saved and the workflow won't change 486 * @return all broken references, by attribute path 487 */ 488 public static Map<String, List<String>> checkBrokenReferences(ModelAwareDataHolder dataHolder, boolean fix) 489 { 490 Map<String, List<String>> result = new HashMap<>(); 491 Map<String, Object> contentAttributes = DataHolderHelper.findItemsByType(dataHolder, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID); 492 for (Map.Entry<String, Object> entry : contentAttributes.entrySet()) 493 { 494 String dataPath = entry.getKey(); 495 Object value = entry.getValue(); 496 if (value instanceof ContentValue) 497 { 498 ContentValue contentValue = (ContentValue) value; 499 if (contentValue.getContentIfExists().isEmpty()) 500 { 501 result.computeIfAbsent(dataPath, __ -> new ArrayList<>()).add(contentValue.getContentId()); 502 if (fix && dataHolder instanceof ModifiableModelAwareDataHolder) 503 { 504 ((ModifiableModelAwareDataHolder) dataHolder).setValue(dataPath, null); 505 } 506 } 507 } 508 else if (value instanceof ContentValue[]) 509 { 510 List<ContentValue> newValues = new ArrayList<>(); 511 for (ContentValue contentValue : (ContentValue[]) value) 512 { 513 if (contentValue.getContentIfExists().isEmpty()) 514 { 515 result.computeIfAbsent(dataPath, __ -> new ArrayList<>()).add(contentValue.getContentId()); 516 } 517 else if (fix) 518 { 519 newValues.add(contentValue); 520 } 521 } 522 if (fix && dataHolder instanceof ModifiableModelAwareDataHolder) 523 { 524 ((ModifiableModelAwareDataHolder) dataHolder).setValue(dataPath, newValues.toArray(ContentValue[]::new)); 525 } 526 } 527 } 528 return result; 529 } 530 531 /** 532 * Find any broken invert relation in a content attributes. 533 * The invert relation is considered as broken if the referenced content does not reference the first one by the attribute with the invert relation path 534 * This check does not take nonexistent referenced contents into account. First call the {@link ContentDataHelper#checkBrokenReferences(ModelAwareDataHolder, boolean)} method 535 * @param content the content to inspect. 536 * @param fix if <code>true</code>, and the referenced contents are modifiable, broken invert relations will be fixed by adding the relation on the referenced content 537 * if the referenced content is referencing another content and the attribute is single (and not in a repeater), the third content will be modified too, to remove the relation to the second content. 538 * Warning: if modifications are made on some contents, the modifications won't be saved and the workflows won't change 539 * @return All broken invert relations in a collection of {@link ReferencedContents}. 540 * Call {@link ReferencedContents#getAddedContents()} to get all contents referenced by the given content that does not reference it in return 541 * Call {@link ReferencedContents#getThirdPartyContents()} to get contents referenced by content in added contents that reference it in return 542 */ 543 public static Collection<ReferencedContents> checkBrokenInvertRelations(Content content, boolean fix) 544 { 545 // Search for all content attribute's values 546 Map<String, Object> contentAttributes = DataHolderHelper.findItemsByType(content, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID); 547 548 // Collect broken invert relations 549 Collection<ReferencedContents> brokenInvertRelations = new ArrayList<>(); 550 for (Map.Entry<String, Object> entry : contentAttributes.entrySet()) 551 { 552 String dataPath = entry.getKey(); 553 Object value = entry.getValue(); 554 ContentAttributeDefinition definition = (ContentAttributeDefinition) content.getDefinition(dataPath); 555 556 // Continue check only on content attributes with an invert relation path 557 if (StringUtils.isNotBlank(definition.getInvertRelationPath())) 558 { 559 Map<ContentValue, List<String>> contentsToAdd = new HashMap<>(); 560 Map<ContentValue, ContentValue> thirdPartyContents = new HashMap<>(); 561 562 if (value instanceof ContentValue) 563 { 564 Pair<Boolean, Optional<ContentValue>> isBroken = _isInvertRelationBroken(content, (ContentValue) value, definition); 565 if (isBroken.getLeft()) 566 { 567 contentsToAdd.put((ContentValue) value, List.of(dataPath)); 568 isBroken.getRight() 569 .ifPresent(v -> thirdPartyContents.put((ContentValue) value, v)); 570 } 571 } 572 else if (value instanceof ContentValue[]) 573 { 574 for (ContentValue contentValue : (ContentValue[]) value) 575 { 576 Pair<Boolean, Optional<ContentValue>> isBroken = _isInvertRelationBroken(content, contentValue, definition); 577 if (isBroken.getLeft()) 578 { 579 contentsToAdd.computeIfAbsent(contentValue, __ -> new ArrayList<>()).add(dataPath); 580 isBroken.getRight() 581 .ifPresent(v -> thirdPartyContents.put(contentValue, v)); 582 } 583 } 584 } 585 586 // Add a ReferencedContents object if there is at least one broken relation 587 // No need to check the third part content, this map can't have entries if the first one is empty 588 if (!contentsToAdd.isEmpty()) 589 { 590 ReferencedContents brokenInvertRelation = new ReferencedContents(definition, contentsToAdd, Map.of(), thirdPartyContents); 591 brokenInvertRelations.add(brokenInvertRelation); 592 } 593 } 594 } 595 596 if (fix) 597 { 598 Map<String, ModifiableContent> modifiedContents = new HashMap<>(); 599 600 for (ReferencedContents brokenInvertRelation : brokenInvertRelations) 601 { 602 // Add relation on contents referenced by the given one 603 String invertRelationPath = brokenInvertRelation.getDefinition().getInvertRelationPath(); 604 modifiedContents.putAll(manageInvertRelations(invertRelationPath, brokenInvertRelation.getAddedContents(), content.getId(), ContentDataHelper::addInvertRelation, modifiedContents)); 605 606 // Remove relations on third party contents 607 Map<ContentValue, Collection<String>> thirdPartyContents = new HashMap<>(); 608 for (Map.Entry<ContentValue, ContentValue> thirdPartyContent : brokenInvertRelation.getThirdPartyContents().entrySet()) 609 { 610 thirdPartyContents.computeIfAbsent(thirdPartyContent.getValue(), __ -> new ArrayList<>()).add(thirdPartyContent.getKey().getContentId()); 611 } 612 modifiedContents.putAll(manageInvertRelations(brokenInvertRelation.getDefinition().getPath(), thirdPartyContents, ContentDataHelper::removeInvertRelation, modifiedContents)); 613 } 614 } 615 616 return brokenInvertRelations; 617 } 618 619 /** 620 * Check if the invert relation between the given contents is broken 621 * @param source the source content of the invert relation 622 * @param destinationValue the destination content of the invert relation 623 * @param definition the definition concerned by the checked invert relation 624 * @return a {@link Pair} containing the result of the check ({@link Boolean} part left) and an optional third part content, if the relation is broken and the destination content references another content in a single attribute that should be modified 625 */ 626 private static Pair<Boolean, Optional<ContentValue>> _isInvertRelationBroken(Content source, ContentValue destinationValue, ContentAttributeDefinition definition) 627 { 628 Boolean isBroken = Boolean.FALSE; 629 Optional<ContentValue> thirdPartContent = Optional.empty(); 630 631 // Get the target content - ignore nonexistent contents, the checkBrokenReferences dealt with it 632 Optional<? extends Content> optDestination = destinationValue.getContentIfExists(); 633 if (optDestination.isPresent()) 634 { 635 // Get the value of the target content's attribute 636 Content destination = optDestination.get(); 637 Object value = destination.getValue(definition.getInvertRelationPath(), true); 638 if (value == null) 639 { 640 isBroken = true; 641 } 642 else if (value instanceof ContentValue) 643 { 644 // If the value does not correspond to the current data holder 645 if (!((ContentValue) value).getContentId().equals(source.getId())) 646 { 647 isBroken = true; 648 649 // If the value of the destination is in relation to the destination too, this third part content will have to be modified 650 Object thirdPartValue = ((ContentValue) value).getValue(definition.getPath(), true); 651 if (thirdPartValue instanceof ContentValue) 652 { 653 if (((ContentValue) thirdPartValue).getContentId().equals(destination.getId())) 654 { 655 thirdPartContent = Optional.of((ContentValue) value); 656 } 657 } 658 else if (thirdPartValue instanceof ContentValue[]) 659 { 660 for (ContentValue thridPartSingleValue : (ContentValue[]) thirdPartValue) 661 { 662 if (thridPartSingleValue.getContentId().equals(destination.getId())) 663 { 664 thirdPartContent = Optional.of((ContentValue) value); 665 break; 666 } 667 } 668 } 669 } 670 } 671 else if (value instanceof ContentValue[]) 672 { 673 // Search for the source content in the values of the destination 674 boolean foundSource = false; 675 for (ContentValue contentValue : (ContentValue[]) value) 676 { 677 if (contentValue.getContentId().equals(source.getId())) 678 { 679 foundSource = true; 680 break; 681 } 682 } 683 684 isBroken = Boolean.valueOf(!foundSource); 685 } 686 } 687 688 return new ImmutablePair<>(isBroken, thirdPartContent); 689 } 690 691 /** 692 * Manages the invert relations concerned by the given path, through the given {@link InvertRelationManager} 693 * @param invertRelationPath the concerned invert relation path 694 * @param referencedContents a {@link Set} containing the referenced contents to manage (add or remove the relation) 695 * @param referencingContentId the id of the content referencing the given contents 696 * @param invertRelationManager the {@link InvertRelationManager} to use (to add or remove the invert relations) 697 * @param alreadyModifiedContents a {@link Map} of contents (indexed by their identifiers) that have already been modified. 698 * This map will be used to initialize the returned map and to search for contents that have already been resolved (to resolve each content only once, even if there are several modifications on a same content) 699 * @return a {@link Map} containing the contents (indexed by their identifiers) that have been modified by this call and the ones that have already been modified 700 */ 701 public static Map<String, ModifiableContent> manageInvertRelations(String invertRelationPath, Set<ContentValue> referencedContents, String referencingContentId, InvertRelationManager invertRelationManager, Map<String, ModifiableContent> alreadyModifiedContents) 702 { 703 Map<ContentValue, Collection<String>> referencedContentsWithReferencing = referencedContents.stream() 704 .collect(Collectors.toMap(value -> value, value -> Set.of(referencingContentId))); 705 706 return manageInvertRelations(invertRelationPath, referencedContentsWithReferencing, invertRelationManager, alreadyModifiedContents); 707 } 708 709 /** 710 * Manages the invert relations concerned by the given path, through the given {@link InvertRelationManager} 711 * @param invertRelationPath the concerned invert relation path 712 * @param referencedContents a {@link Map} containing the referenced contents to manage (add or remove the relation) and the id of the content referencing this content 713 * @param invertRelationManager the {@link InvertRelationManager} to use (to add or remove the invert relations) 714 * @param alreadyModifiedContents a {@link Map} of contents (indexed by their identifiers) that have already been modified. 715 * This map will be used to initialize the returned map and to search for contents that have already been resolved (to resolve each content only once, even if there are several modifications on a same content) 716 * @return a {@link Map} containing the contents (indexed by their identifiers) that have been modified by this call and the ones that have already been modified 717 */ 718 public static Map<String, ModifiableContent> manageInvertRelations(String invertRelationPath, Map<ContentValue, Collection<String>> referencedContents, InvertRelationManager invertRelationManager, Map<String, ModifiableContent> alreadyModifiedContents) 719 { 720 Map<String, ModifiableContent> allModifiedContents = new HashMap<>(alreadyModifiedContents); 721 722 for (ContentValue referencedContent : referencedContents.keySet()) 723 { 724 String contentId = referencedContent.getContentId(); 725 Optional<ModifiableContent> optContent = allModifiedContents.containsKey(contentId) 726 ? Optional.of(allModifiedContents.get(contentId)) 727 : referencedContent.getContentIfExists(); 728 729 if (optContent.isPresent()) 730 { 731 ModifiableContent content = optContent.get(); 732 733 ModelItem modelItem = content.getDefinition(invertRelationPath); 734 ValueContext valueContext = ValueContext.newInstance(); 735 if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(content, modelItem)) 736 { 737 if (ExternalizableDataStatus.EXTERNAL.equals(content.getStatus(invertRelationPath))) 738 { 739 throw new IllegalStateException("Unable to manage the invert relation on distant content '" + content + "', the data at path '" + invertRelationPath + "' is external."); 740 } 741 else 742 { 743 valueContext.withStatus(ExternalizableDataStatus.LOCAL); 744 } 745 } 746 747 if (invertRelationManager.manageInvertRelation(content, invertRelationPath, referencedContents.get(referencedContent), valueContext)) 748 { 749 allModifiedContents.put(contentId, content); 750 } 751 } 752 } 753 754 return allModifiedContents; 755 } 756 757 /** 758 * Adds the invert relation to the given {@link DataHolder} 759 * @param dataHolder the data holder where to add the invert relation 760 * @param invertRelationPath the path of the invert relation 761 * @param referencingContentIds the id of the contents that reference the data holder 762 * @param context the value's context 763 * @return <code>true</code> if the given data holder has been modified, <code>false</code> otherwise 764 */ 765 public static boolean addInvertRelation(ModifiableModelAwareDataHolder dataHolder, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context) 766 { 767 String[] pathSegments = StringUtils.split(invertRelationPath, ModelItem.ITEM_PATH_SEPARATOR); 768 String dataName = pathSegments[0]; 769 770 if (pathSegments.length == 1) 771 { 772 ContentAttributeDefinition invertRelationDefinition = (ContentAttributeDefinition) dataHolder.getDefinition(invertRelationPath); 773 if (invertRelationDefinition.isMultiple()) 774 { 775 List<String> newValues = new ArrayList<>(); 776 777 if (DataHolderHelper.hasValue(dataHolder, invertRelationPath, context)) 778 { 779 ContentValue[] oldValues = DataHolderHelper.getValue(dataHolder, invertRelationPath, context); 780 781 newValues = Arrays.stream(oldValues) 782 .map(ContentValue::getContentId) 783 .collect(Collectors.toList()); 784 } 785 786 newValues.addAll(referencingContentIds); 787 DataHolderHelper.setValue(dataHolder, invertRelationPath, newValues.toArray(new String[newValues.size()]), context); 788 return true; 789 } 790 else 791 { 792 assert referencingContentIds.size() == 1; 793 DataHolderHelper.setValue(dataHolder, invertRelationPath, referencingContentIds.iterator().next(), context); 794 return true; 795 } 796 } 797 else 798 { 799 ModelItem modelItem = dataHolder.getDefinition(dataName); 800 String subInvertRelationPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 801 if (modelItem instanceof RepeaterDefinition) 802 { 803 ModifiableModelAwareRepeaterEntry repeaterEntry = dataHolder.getRepeater(dataName, true).addEntry(); 804 return addInvertRelation(repeaterEntry, subInvertRelationPath, referencingContentIds, context); 805 } 806 else 807 { 808 ModifiableModelAwareComposite composite = dataHolder.getComposite(dataName, true); 809 return addInvertRelation(composite, subInvertRelationPath, referencingContentIds, context); 810 } 811 } 812 } 813 814 /** 815 * Removes the invert relation to the given {@link DataHolder} 816 * @param referencedContent the content where to remove the invert relation 817 * @param invertRelationPath the path of the invert relation 818 * @param referencingContentIds the id of the contents that do not reference the given content anymore 819 * @param context the value's context 820 * @return <code>true</code> if the referenced content has been modified, <code>false</code> otherwise 821 */ 822 public static boolean removeInvertRelation(ModifiableContent referencedContent, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context) 823 { 824 ContentAttributeDefinition invertRelationDefinition = (ContentAttributeDefinition) referencedContent.getDefinition(invertRelationPath); 825 if (invertRelationDefinition.isMultiple()) 826 { 827 if (DataHolderHelper.hasValue(referencedContent, invertRelationPath, context)) 828 { 829 ContentValue[] oldValues = DataHolderHelper.getValue(referencedContent, invertRelationPath, context); 830 831 String[] newValues = Arrays.stream(oldValues) 832 .map(ContentValue::getContentId) 833 .filter(id -> !referencingContentIds.contains(id)) 834 .toArray(size -> new String[size]); 835 836 DataHolderHelper.setValue(referencedContent, invertRelationPath, newValues, context); 837 return oldValues.length > newValues.length; 838 } 839 else 840 { 841 return false; 842 } 843 } 844 else 845 { 846 return _removeSingleInvertRelation(referencedContent, invertRelationPath, referencingContentIds, context); 847 } 848 } 849 850 private static boolean _removeSingleInvertRelation(ModifiableModelAwareDataHolder dataHolder, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context) 851 { 852 String[] pathSegments = StringUtils.split(invertRelationPath, ModelItem.ITEM_PATH_SEPARATOR); 853 String dataName = pathSegments[0]; 854 855 if (pathSegments.length == 1) 856 { 857 ContentValue value = DataHolderHelper.getValue(dataHolder, dataName, context); 858 if (value != null && referencingContentIds.contains(value.getContentId())) 859 { 860 DataHolderHelper.removeValue(dataHolder, dataName, context); 861 return true; 862 } 863 else 864 { 865 return false; 866 } 867 } 868 else 869 { 870 ModelItem modelItem = dataHolder.getDefinition(dataName); 871 String subInvertRelationPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 872 if (modelItem instanceof RepeaterDefinition) 873 { 874 boolean modified = false; 875 876 ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(dataName); 877 if (repeater != null) 878 { 879 for (ModifiableModelAwareRepeaterEntry repeaterEntry : repeater.getEntries()) 880 { 881 modified = _removeSingleInvertRelation(repeaterEntry, subInvertRelationPath, referencingContentIds, context) || modified; 882 } 883 } 884 885 return modified; 886 } 887 else 888 { 889 ModifiableModelAwareComposite composite = dataHolder.getComposite(dataName); 890 return _removeSingleInvertRelation(composite, subInvertRelationPath, referencingContentIds, context); 891 } 892 } 893 } 894 895 /** 896 * Invert relation manager (to add or remove a relation on a content 897 */ 898 @FunctionalInterface 899 public interface InvertRelationManager 900 { 901 /** 902 * Manages the invert relation to the given {@link DataHolder} 903 * @param referencedContent the content where to manage the invert relation 904 * @param invertRelationPath the path of the invert relation 905 * @param referencingContentIds the id of the contents that reference.d the given content 906 * @param context the value's context 907 * @return <code>true</code> if the referenced content has been modified, <code>false</code> otherwise 908 */ 909 public boolean manageInvertRelation(ModifiableContent referencedContent, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context); 910 } 911}