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