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