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