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.cms.content.referencetable; 017 018import java.text.Normalizer; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028 029import org.apache.avalon.framework.activity.Disposable; 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.ArrayUtils; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.cms.contenttype.ContentAttributeDefinition; 038import org.ametys.cms.contenttype.ContentType; 039import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 040import org.ametys.cms.data.ContentValue; 041import org.ametys.cms.repository.Content; 042import org.ametys.cms.repository.ContentQueryHelper; 043import org.ametys.cms.repository.ContentTypeExpression; 044import org.ametys.cms.repository.MixinTypeExpression; 045import org.ametys.core.ui.Callable; 046import org.ametys.plugins.repository.AmetysObjectIterable; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.EmptyIterable; 049import org.ametys.plugins.repository.query.SortCriteria; 050import org.ametys.plugins.repository.query.expression.AndExpression; 051import org.ametys.plugins.repository.query.expression.Expression; 052import org.ametys.plugins.repository.query.expression.Expression.Operator; 053import org.ametys.plugins.repository.query.expression.MetadataExpression; 054import org.ametys.plugins.repository.query.expression.NotExpression; 055import org.ametys.plugins.repository.query.expression.OrExpression; 056import org.ametys.plugins.repository.query.expression.StringExpression; 057import org.ametys.runtime.model.Model; 058import org.ametys.runtime.model.ModelItem; 059import org.ametys.runtime.plugin.component.AbstractLogEnabled; 060 061import com.google.common.collect.BiMap; 062import com.google.common.collect.HashBiMap; 063 064/** 065 * Helper component for computing information about hierarchy of reference table Contents. 066 * <br><br> 067 * 068 * At the startup of the Ametys application, you must call {@link #registerRelation(ContentType, ContentType)} in order to register a <b>relation</b> between a <b>parent</b> content type and its <b>child</b> content type.<br> 069 * When all relations are registered, one or several <b>hierarchy(ies)</b> can be inferred, following some basic rules:<br> 070 * <ul> 071 * <li>A hierarchy of two or more content types cannot be cyclic</li> 072 * <li>A content type can have itself as its parent content type</li> 073 * <li>A content type cannot have two different parent content types</li> 074 * <li>A content type can have only one content type as children, plus possibly itself</li> 075 * </ul> 076 * From each hierarchy of content types, a <b>tree</b> of contents can be inferred.<br> 077 * <br> 078 * For instance, the following examples of hierarchy are valid (where <b>X←Y</b> means 'X is the parent content type of Y'; <br>and <b>⤹Z</b> means 'Z is the parent content type of Z'): 079 * <ul> 080 * <li>B←A</li> 081 * <li>E←D←C←B←A</li> 082 * <li>⤹B←A (content type B defines two different child content types, but one is itself, which is allowed)</li> 083 * <li>⤹E←D←C←B←A</li> 084 * <li>⤹A</li> 085 * </ul> 086 * ; and the following examples of hierarchy are invalid: 087 * <ul> 088 * <li>C←B and C←A (a content type cannot have multiple content types as children, which are not itself)</li> 089 * <li>C←A and B←A (a content type cannot have two different parent content types)</li> 090 * <li>⤹A and B←A (a content type cannot have two different parent content types, even if one is itself)</li> 091 * <li>A←B and B←A (cyclic hierarchy)</li> 092 * <li>A←C←B←A (cyclic hierarchy)</li> 093 * </ul> 094 */ 095public class HierarchicalReferenceTablesHelper extends AbstractLogEnabled implements Component, Serviceable, Disposable 096{ 097 /** The Avalon role */ 098 public static final String ROLE = HierarchicalReferenceTablesHelper.class.getName(); 099 /** the candidate content type id*/ 100 public static final String CANDIDATE_CONTENT_TYPE = "org.ametys.cms.referencetable.mixin.Candidate"; 101 /** Tag for reference table */ 102 private static final String TAG_CANDIDATE = "allow-candidates"; 103 104 /** The extension point for content types */ 105 protected ContentTypeExtensionPoint _contentTypeEP; 106 /** The Ametys objet resolver */ 107 protected AmetysObjectResolver _resolver; 108 109 /** The map parent -> child (excepted content types pointing on themselves) */ 110 private BiMap<ContentType, ContentType> _childByContentType = HashBiMap.create(); 111 /** The content types pointing at themselves */ 112 private Set<ContentType> _autoReferencingContentTypes = new HashSet<>(); 113 /** The map leafContentType -> topLevelContentType */ 114 private BiMap<ContentType, ContentType> _topLevelTypeByLeafType = HashBiMap.create(); 115 116 @Override 117 public void service(ServiceManager manager) throws ServiceException 118 { 119 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 120 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 121 } 122 123 @Override 124 public void dispose() 125 { 126 _childByContentType.clear(); 127 _autoReferencingContentTypes.clear(); 128 _topLevelTypeByLeafType.clear(); 129 } 130 131 /** 132 * Register a relation between a parent and its child, and update the internal model if it is a valid one. 133 * @param parent The parent content type 134 * @param child The child content type 135 * @return true if the relation is valid, i.e. in accordance with what was registered before 136 */ 137 public boolean registerRelation(ContentType parent, ContentType child) 138 { 139 if (parent.equals(child)) 140 { 141 _autoReferencingContentTypes.add(parent); 142 if (_childByContentType.containsKey(parent)) 143 { 144 // _topLevelTypeByLeafType does not need to be updated as another content type references it 145 } 146 else 147 { 148 // _topLevelTypeByLeafType needs to be updated as no other content type references it 149 _topLevelTypeByLeafType.put(parent, parent); 150 } 151 return true; 152 } 153 else if (_childByContentType.containsKey(parent)) 154 { 155 getLogger().error("Problem of definition of parent for content type '{}'. Its parent '{}' is already declared by '{}'. A content type cannot have multiple content types as children", child, parent, _childByContentType.get(parent)); 156 return false; 157 } 158 else if (_checkNoCycle(parent, child)) 159 { 160 // ok valid 161 // update _childByContentType 162 _childByContentType.put(parent, child); 163 164 // update _topLevelTypeByLeafType 165 boolean containsParentAsKey = _topLevelTypeByLeafType.containsKey(parent); 166 boolean containsChildAsValue = _topLevelTypeByLeafType.containsValue(child); 167 if (containsParentAsKey && containsChildAsValue) 168 { 169 // is currently something as {parent: other, other2: child}, which means the hierarchy is like: other -> parent -> child -> other2 170 // we now want {other2: other} 171 ContentType other = _topLevelTypeByLeafType.remove(parent); 172 ContentType other2 = _topLevelTypeByLeafType.inverse().get(child); 173 _topLevelTypeByLeafType.put(other2, other); 174 } 175 else if (containsParentAsKey) 176 { 177 // is currently something as {parent: other}, which means the hierarchy is like: other -> ... -> parent -> child 178 // we now want {child: other} 179 ContentType other = _topLevelTypeByLeafType.remove(parent); 180 _topLevelTypeByLeafType.put(child, other); 181 } 182 else if (containsChildAsValue) 183 { 184 // is currently something as {other: child}, which means the hierarchy is like: parent -> child -> ... -> other 185 // we now want {other: parent} 186 ContentType other = _topLevelTypeByLeafType.inverse().get(child); 187 _topLevelTypeByLeafType.put(other, parent); 188 } 189 else 190 { 191 _topLevelTypeByLeafType.put(child, parent); 192 } 193 return true; 194 } 195 else 196 { 197 // An error was logged in #_checkNoCycle method 198 return false; 199 } 200 } 201 202 /** 203 * Returns true if at least one hierarchy was registered (i.e. at least one content type defines a valid "parent" metadata) 204 * @return true if at least one hierarchy was registered 205 */ 206 public boolean hasAtLeastOneHierarchy() 207 { 208 return !_childByContentType.isEmpty() || !_autoReferencingContentTypes.isEmpty(); 209 } 210 211 /** 212 * Gets the top level content type for the given leaf content type (which defines the hierarchy) 213 * @param leafContentType the leaf cotnent type 214 * @return the top level content type for the given leaf content type 215 */ 216 public ContentType getTopLevelType(ContentType leafContentType) 217 { 218 return _topLevelTypeByLeafType.get(leafContentType); 219 } 220 221 private boolean _checkNoCycle(ContentType parent, ContentType child) 222 { 223 // at this stage, parent is not equal to child 224 225 Set<ContentType> contentTypesInHierarchy = new HashSet<>(); 226 contentTypesInHierarchy.add(child); 227 contentTypesInHierarchy.add(parent); 228 229 ContentType parentContentType = parent; 230 231 do 232 { 233 final ContentType currentContentType = parentContentType; 234 parentContentType = Optional.ofNullable(currentContentType) 235 .map(ContentType::getParentAttributeDefinition) 236 .filter(opt -> !opt.isEmpty()) 237 .map(Optional::get) 238 .map(ModelItem::getModel) 239 .map(Model::getId) 240 .map(_contentTypeEP::getExtension) 241 .filter(par -> !currentContentType.equals(par)) // if current equals to parent, set to null as there will be no cycle 242 .orElse(null); 243 244 if (contentTypesInHierarchy.contains(parentContentType)) 245 { 246 // there is a cycle, log an error and return false 247 getLogger().error("Problem of definition of parent for content type {}. It defines a cyclic hierarchy (The following content types are involved: {}).", child, contentTypesInHierarchy); 248 return false; 249 } 250 251 contentTypesInHierarchy.add(parentContentType); 252 } 253 while (parentContentType != null); 254 255 // no cycle, it is ok, return true 256 return true; 257 } 258 259 /** 260 * Returns true if the given content type has a child content type 261 * @param contentType The content type 262 * @return true if the given content type has a child content type 263 */ 264 public boolean hasChildContentType(ContentType contentType) 265 { 266 return _childByContentType.containsKey(contentType) || _autoReferencingContentTypes.contains(contentType); 267 } 268 269 /** 270 * Returns true if the given content has a hierarchical content type, i.e. is part of a hierarchy 271 * @param content The content 272 * @return true if the given content is part of a hierarchical reference table 273 */ 274 public boolean isHierarchical(Content content) 275 { 276 String[] types = content.getTypes(); 277 for (String type : types) 278 { 279 ContentType contentType = _contentTypeEP.getExtension(type); 280 if (isHierarchical(contentType)) 281 { 282 return true; 283 } 284 } 285 286 return false; 287 } 288 289 /** 290 * Returns true if the given content type is hierarchical, i.e. is part of a hierarchy 291 * @param contentType The content type 292 * @return true if the given content type is hierarchical, i.e. is part of a hierarchy 293 */ 294 public boolean isHierarchical(ContentType contentType) 295 { 296 return _childByContentType.containsKey(contentType) || _childByContentType.containsValue(contentType) || _autoReferencingContentTypes.contains(contentType); 297 } 298 299 /** 300 * Returns true if the given content type is a leaf content type 301 * @param contentType The content type 302 * @return true if the given content type is a leaf content type 303 */ 304 public boolean isLeaf(ContentType contentType) 305 { 306 return _topLevelTypeByLeafType.containsKey(contentType); 307 } 308 309 /** 310 * Get the hierarchy of content types (distinct content types) 311 * @param leafContentTypeId The id of leaf content type 312 * @return The content types of hierarchy 313 */ 314 public Set<String> getHierarchicalContentTypes(String leafContentTypeId) 315 { 316 Set<String> hierarchicalTypes = new LinkedHashSet<>(); 317 hierarchicalTypes.add(leafContentTypeId); 318 BiMap<ContentType, ContentType> parentByContentType = _childByContentType.inverse(); 319 320 ContentType leafContentType = _contentTypeEP.getExtension(leafContentTypeId); 321 322 ContentType parentContentType = parentByContentType.get(leafContentType); 323 while (parentContentType != null) 324 { 325 hierarchicalTypes.add(parentContentType.getId()); 326 parentContentType = parentByContentType.get(parentContentType); 327 } 328 return hierarchicalTypes; 329 } 330 331 /** 332 * Get the path of reference table entry in its hierarchy 333 * @param refTableEntryId The id of entry 334 * @return The path from root parent 335 */ 336 @Callable 337 public Map<String, String> getPathInHierarchy(List<String> refTableEntryId) 338 { 339 Map<String, String> paths = new HashMap<>(); 340 341 for (String id : refTableEntryId) 342 { 343 paths.put(id, getPathInHierarchy(id)); 344 } 345 346 return paths; 347 } 348 349 /** 350 * Get the path of reference table entry in its hierarchy 351 * @param refTableEntryId The id of entry 352 * @return The path from root parent 353 */ 354 @Callable 355 public String getPathInHierarchy(String refTableEntryId) 356 { 357 Content refTableEntry = _resolver.resolveById(refTableEntryId); 358 List<String> paths = new ArrayList<>(); 359 paths.add(refTableEntry.getName()); 360 361 String parentId = getParent(refTableEntry); 362 while (parentId != null) 363 { 364 Content parent = _resolver.resolveById(parentId); 365 paths.add(parent.getName()); 366 parentId = getParent(parent); 367 } 368 369 Collections.reverse(paths); 370 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 371 } 372 373 /** 374 * Gets the content types the children of the given content can have. 375 * The result can contain 0, 1 or 2 content types 376 * @param refTableEntry The content 377 * @return the content types the children of the given content can have. 378 */ 379 public List<ContentType> getChildContentTypes(Content refTableEntry) 380 { 381 List<ContentType> result = new ArrayList<>(); 382 383 for (String cTypeId : refTableEntry.getTypes()) 384 { 385 ContentType cType = _contentTypeEP.getExtension(cTypeId); 386 if (_childByContentType.containsKey(cType)) 387 { 388 result.add(_childByContentType.get(cType)); 389 } 390 if (_autoReferencingContentTypes.contains(cType)) 391 { 392 result.add(cType); 393 } 394 395 if (!result.isEmpty()) 396 { 397 break; 398 } 399 } 400 401 return result; 402 } 403 404 /** 405 * Get the metadata values of a candidate 406 * @param contentId the id of candidate 407 * @return the candidate's values 408 */ 409 @Callable 410 public Map<String, Object> getCandidateValues(String contentId) 411 { 412 Map<String, Object> values = new HashMap<>(); 413 414 Content content = _resolver.resolveById(contentId); 415 values.put("title", content.getValue(Content.ATTRIBUTE_TITLE)); 416 values.put("comment", content.getValue("comment")); 417 418 return values; 419 } 420 421 /** 422 * Get the parent metadata 423 * @param contentId The content id 424 * @return the path of parent metadata or null if not found 425 */ 426 @Callable 427 public String getParentAttribute(String contentId) 428 { 429 Content content = _resolver.resolveById(contentId); 430 return getParentAttribute(content); 431 } 432 433 /** 434 * Get the parent metadata 435 * @param content The content 436 * @return the path of parent metadata or null if not found 437 */ 438 public String getParentAttribute(Content content) 439 { 440 for (String cTypeId : content.getTypes()) 441 { 442 ContentType cType = _contentTypeEP.getExtension(cTypeId); 443 Optional<ContentAttributeDefinition> parentMetadata = cType.getParentAttributeDefinition(); 444 if (!parentMetadata.isEmpty()) 445 { 446 return parentMetadata.get().getPath(); 447 } 448 } 449 450 return null; 451 } 452 453 /** 454 * Returns the "parent" attribute value for the given content, or null if it is not defined for its content types 455 * See also {@link ContentType#getParentAttributeDefinition()} 456 * @param content The content 457 * @return the "parent" attribute value for the given content, or null 458 */ 459 public String getParent(Content content) 460 { 461 for (String cTypeId : content.getTypes()) 462 { 463 if (!_contentTypeEP.hasExtension(cTypeId)) 464 { 465 getLogger().warn("The content {} ({}) has unknown type '{}'", content.getId(), content.getTitle(), cTypeId); 466 continue; 467 } 468 469 ContentType cType = _contentTypeEP.getExtension(cTypeId); 470 Optional<String> contentId = cType.getParentAttributeDefinition() 471 .map(ContentAttributeDefinition::getName) 472 .filter(name -> content.hasValue(name)) 473 .map(name -> content.getValue(name)) 474 .map(value -> ((ContentValue) value).getContentId()); 475 476 if (!contentId.isEmpty()) 477 { 478 return contentId.get(); 479 } 480 } 481 return null; 482 } 483 484 /** 485 * Recursively returns the "parent" metadata value for the given content, i.e; will return the parent, the parent of the parent, etc. 486 * @param content The content 487 * @return all the parents of the given content 488 */ 489 public List<String> getAllParents(Content content) 490 { 491 List<String> parents = new ArrayList<>(); 492 493 Content currentContent = content; 494 String parentId = getParent(currentContent); 495 496 while (parentId != null) 497 { 498 if (_resolver.hasAmetysObjectForId(parentId)) 499 { 500 parents.add(parentId); 501 currentContent = _resolver.resolveById(parentId); 502 parentId = getParent(currentContent); 503 } 504 else 505 { 506 break; 507 } 508 } 509 510 return parents; 511 } 512 513 /** 514 * Returns the direct children of a content 515 * @param content the content to get the direct children from 516 * @return the AmetysObjectIterable of the direct children of the content 517 */ 518 public AmetysObjectIterable<Content> getDirectChildren(Content content) 519 { 520 List<ContentType> childContentTypes = getChildContentTypes(content); 521 522 if (!childContentTypes.isEmpty()) 523 { 524 List<Expression> exprs = new ArrayList<>(); 525 for (ContentType childContentType : childContentTypes) 526 { 527 String pointingMetadataName = childContentType.getParentAttributeDefinition() 528 .map(ModelItem::getName) 529 .orElse(StringUtils.EMPTY); 530 531 if (StringUtils.isNotEmpty(pointingMetadataName)) 532 { 533 // //element(*, ametys:content)[@ametys-internal:contentType = 'foo.child.contentType' and @ametys:pointingMetadata = 'contentId'] 534 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, childContentType.getId()); 535 Expression parentExpr = new StringExpression(pointingMetadataName, Operator.EQ, content.getId()); 536 537 exprs.add(new AndExpression(cTypeExpr, parentExpr)); 538 } 539 } 540 541 Expression finalExpr = new OrExpression(exprs.toArray(new Expression[exprs.size()])); 542 543 SortCriteria sortCriteria = new SortCriteria(); 544 sortCriteria.addCriterion("title", true, true); 545 String xPathQuery = ContentQueryHelper.getContentXPathQuery(finalExpr, sortCriteria); 546 547 return _resolver.query(xPathQuery); 548 } 549 550 return new EmptyIterable<>(); 551 } 552 553 /** 554 * Returns a Set of all the descendants 555 * @param content the content to get the children from 556 * @return the Set of children 557 */ 558 public Set<String> getAllChildren(Content content) 559 { 560 AmetysObjectIterable<Content> directChildren = getDirectChildren(content); 561 Set<String> children = new HashSet<>(); 562 for (Content child : directChildren) 563 { 564 children.add(child.getId()); 565 children.addAll(getAllChildren(child)); 566 } 567 return children; 568 } 569 570 /** 571 * Return a boolean value to determine if all the contents are simple 572 * @param contentTypeLeaf the contentType id of the leaf content 573 * @return true if all the contents in the tree are simple, false otherwise 574 */ 575 @Callable 576 public boolean isHierarchicalSimpleTree(String contentTypeLeaf) 577 { 578 Set<String> cTypeIds = getHierarchicalContentTypes(contentTypeLeaf); 579 for (String cTypeId : cTypeIds) 580 { 581 ContentType cType = _contentTypeEP.getExtension(cTypeId); 582 if (cType != null && !cType.isSimple()) 583 { 584 return false; 585 } 586 } 587 588 return true; 589 } 590 591 /** 592 * Return the contents at the root 593 * @param rootContentType the content type of the contents at the root 594 * @return the contents at the root 595 */ 596 public AmetysObjectIterable<Content> getRootChildren(ContentType rootContentType) 597 { 598 return getRootChildren(rootContentType, false); 599 } 600 601 /** 602 * Return the contents at the root 603 * @param rootContentType the content type of the contents at the root 604 * @param excludeCandidates true to exclude candidates 605 * @return the contents at the root 606 */ 607 public AmetysObjectIterable<Content> getRootChildren(ContentType rootContentType, boolean excludeCandidates) 608 { 609 List<Expression> exprs = new ArrayList<>(); 610 611 exprs.add(new ContentTypeExpression(Operator.EQ, rootContentType.getId())); 612 613 // even if it is the top level type, parent attribute can be not null if it references itself as parent 614 rootContentType.getParentAttributeDefinition() 615 .filter(attribute -> rootContentType.getId().equals(attribute.getContentTypeId())) 616 .ifPresent(attribute -> exprs.add(new NotExpression(new MetadataExpression(attribute.getName())))); 617 618 if (excludeCandidates) 619 { 620 exprs.add(new NotExpression(new MixinTypeExpression(Operator.EQ, CANDIDATE_CONTENT_TYPE))); 621 } 622 623 SortCriteria sort = new SortCriteria(); 624 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 625 626 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(exprs.toArray(new Expression[exprs.size()])), sort); 627 return _resolver.query(query); 628 } 629 630 /** 631 * Return the candidates for given content type 632 * @param cTypeId the id of content type 633 * @return the candidates 634 */ 635 public AmetysObjectIterable<Content> getCandidates(String cTypeId) 636 { 637 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, cTypeId); 638 Expression candidateExpr = new MixinTypeExpression(Operator.EQ, CANDIDATE_CONTENT_TYPE); 639 640 SortCriteria sort = new SortCriteria(); 641 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 642 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, candidateExpr), sort); 643 return _resolver.query(query); 644 } 645 646 /** 647 * Get the path of node which match filter regexp 648 * @param value the value to match 649 * @param contentId the content id from where we will filter 650 * @param leafContentType the leaf content type 651 * @return the matching paths 652 */ 653 @Callable 654 public List<String> filterReferenceTablesByRegExp (String value, String contentId, String leafContentType) 655 { 656 List<String> results = new ArrayList<>(); 657 ContentType topLevelContentType = getTopLevelType(_contentTypeEP.getExtension(leafContentType)); 658 try (AmetysObjectIterable<Content> contents = "root".equals(contentId) ? getRootChildren(topLevelContentType) : getDirectChildren((Content) _resolver.resolveById(contentId))) 659 { 660 for (Content content: contents) 661 { 662 _getMatchingPathsFromContent(content, value, results); 663 } 664 } 665 return results; 666 } 667 668 private void _getMatchingPathsFromContent(Content parent, String value, List<String> result) 669 { 670 String toMatch = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 671 672 String normalizedName = Normalizer.normalize(parent.getTitle().toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 673 674 if (normalizedName.contains(toMatch)) 675 { 676 result.add(getPathInHierarchy(parent.getId())); 677 } 678 679 AmetysObjectIterable<Content> directChildren = getDirectChildren(parent); 680 for (Content child : directChildren) 681 { 682 _getMatchingPathsFromContent(child, value, result); 683 } 684 } 685 686 /** 687 * Determines if this content type supports candidates 688 * @param contentType the content type 689 * @return true if the the candidates are allowed, false otherwise 690 */ 691 public boolean supportCandidates(ContentType contentType) 692 { 693 return contentType.hasTag(TAG_CANDIDATE); 694 } 695 696 /** 697 * Determines if this content type supports candidates 698 * @param contentTypeId the id of content type 699 * @return true if the the candidates are allowed, false otherwise 700 */ 701 @Callable 702 public boolean supportCandidates(String contentTypeId) 703 { 704 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 705 return supportCandidates(contentType); 706 } 707 708 /** 709 * Check if the content is a candidate 710 * @param content the content to test 711 * @return true if the content is a candidate, false otherwise 712 */ 713 public boolean isCandidate(Content content) 714 { 715 return ArrayUtils.contains(content.getMixinTypes(), CANDIDATE_CONTENT_TYPE); 716 } 717 718 /** 719 * Check if the referencing content is the parent of the content 720 * @param referencingContentId The referencing content if to check if it's the parent or not 721 * @param content the child content 722 * @return true if it's the parent, false otherwise 723 */ 724 protected boolean _isParent(String referencingContentId, Content content) 725 { 726 return Optional.of(content) 727 .map(this::getParentAttribute) 728 .map(name -> content.getValue(name)) 729 .map(ContentValue.class::cast) 730 .map(ContentValue::getContentId) 731 .map(parent -> referencingContentId.equals(parent)) 732 .orElse(false); 733 } 734 735 /** 736 * Test if content is referenced by other contents than its parent and children 737 * @param content The content to test 738 * @param contentsId The list of contents id to delete 739 * @return true if content is referenced 740 */ 741 protected boolean _isContentReferenced(Content content, List<String> contentsId) 742 { 743 return _isContentReferenced(content, contentsId, new HashSet<>()); 744 } 745 746 private boolean _isContentReferenced(Content content, List<String> contentsId, Set<String> alreadyVisited) 747 { 748 // If the content is not currently calculated or has already been calculated (avoid infinite loop) 749 if (alreadyVisited.add(content.getId())) 750 { 751 // For each referencing content 752 for (Content referencingContent : content.getReferencingContents()) 753 { 754 String referencingContentId = referencingContent.getId(); 755 756 // If parent, content is not really referenced 757 if (!_isParent(referencingContentId, content)) 758 { 759 // If the referencing content will be deleted or is a direct children of the current content (should be in the first list) 760 // Then control if the content is referenced in another way 761 if (contentsId.contains(referencingContentId) || _isParent(content.getId(), referencingContent)) 762 { 763 if (_isContentReferenced(referencingContent, contentsId, alreadyVisited)) 764 { 765 return true; 766 } 767 } 768 // The referencing content is not planned for deletion then the content is referenced 769 else 770 { 771 return true; 772 } 773 } 774 } 775 } 776 777 return false; 778 } 779}