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