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 .map(ContentType::getParentAttributeDefinition) 250 .filter(opt -> !opt.isEmpty()) 251 .map(Optional::get) 252 .map(ModelItem::getModel) 253 .map(Model::getId) 254 .map(_contentTypeEP::getExtension) 255 .filter(par -> !currentContentType.equals(par)) // if current equals to parent, set to null as there will be no cycle 256 .orElse(null); 257 258 if (contentTypesInHierarchy.contains(parentContentType)) 259 { 260 // there is a cycle, log an error and return false 261 getLogger().error("Problem of definition of parent for content type {}. It defines a cyclic hierarchy (The following content types are involved: {}).", child, contentTypesInHierarchy); 262 return false; 263 } 264 265 contentTypesInHierarchy.add(parentContentType); 266 } 267 while (parentContentType != null); 268 269 // no cycle, it is ok, return true 270 return true; 271 } 272 273 /** 274 * Returns true if the given content type has a child content type 275 * @param contentType The content type 276 * @return true if the given content type has a child content type 277 */ 278 public boolean hasChildContentType(ContentType contentType) 279 { 280 return _childByContentType.containsKey(contentType) || _autoReferencingContentTypes.contains(contentType); 281 } 282 283 /** 284 * Returns true if the given content has a hierarchical content type, i.e. is part of a hierarchy 285 * @param content The content 286 * @return true if the given content is part of a hierarchical reference table 287 */ 288 public boolean isHierarchical(Content content) 289 { 290 String[] types = content.getTypes(); 291 for (String type : types) 292 { 293 ContentType contentType = _contentTypeEP.getExtension(type); 294 if (isHierarchical(contentType)) 295 { 296 return true; 297 } 298 } 299 300 return false; 301 } 302 303 /** 304 * Returns true if the given content type is hierarchical, i.e. is part of a hierarchy 305 * @param contentType The content type 306 * @return true if the given content type is hierarchical, i.e. is part of a hierarchy 307 */ 308 public boolean isHierarchical(ContentType contentType) 309 { 310 return _childByContentType.containsKey(contentType) || _childByContentType.containsValue(contentType) || _autoReferencingContentTypes.contains(contentType); 311 } 312 313 /** 314 * Returns true if the given content type is a leaf content type 315 * @param contentType The content type 316 * @return true if the given content type is a leaf content type 317 */ 318 public boolean isLeaf(ContentType contentType) 319 { 320 return _topLevelTypeByLeafType.containsKey(contentType); 321 } 322 323 /** 324 * Get the hierarchy of content types (distinct content types) 325 * @param leafContentTypeId The id of leaf content type 326 * @return The content types of hierarchy 327 */ 328 public Set<String> getHierarchicalContentTypes(String leafContentTypeId) 329 { 330 Set<String> hierarchicalTypes = new LinkedHashSet<>(); 331 hierarchicalTypes.add(leafContentTypeId); 332 BiMap<ContentType, ContentType> parentByContentType = _childByContentType.inverse(); 333 334 ContentType leafContentType = _contentTypeEP.getExtension(leafContentTypeId); 335 336 ContentType parentContentType = parentByContentType.get(leafContentType); 337 while (parentContentType != null) 338 { 339 hierarchicalTypes.add(parentContentType.getId()); 340 parentContentType = parentByContentType.get(parentContentType); 341 } 342 return hierarchicalTypes; 343 } 344 345 /** 346 * Get the path of reference table entry in its hierarchy 347 * @param refTableEntryId The id of entry 348 * @return The path from root parent 349 */ 350 @Callable 351 public Map<String, String> getPathInHierarchy(List<String> refTableEntryId) 352 { 353 Map<String, String> paths = new HashMap<>(); 354 355 for (String id : refTableEntryId) 356 { 357 paths.put(id, getPathInHierarchy(id)); 358 } 359 360 return paths; 361 } 362 363 /** 364 * Get the path of reference table entry in its hierarchy 365 * @param refTableEntryId The id of entry 366 * @return The path from root parent 367 */ 368 @Callable 369 public String getPathInHierarchy(String refTableEntryId) 370 { 371 Content refTableEntry = _resolver.resolveById(refTableEntryId); 372 List<String> paths = new ArrayList<>(); 373 paths.add(refTableEntry.getName()); 374 375 String parentId = getParent(refTableEntry); 376 while (parentId != null) 377 { 378 Content parent = _resolver.resolveById(parentId); 379 paths.add(parent.getName()); 380 parentId = getParent(parent); 381 } 382 383 Collections.reverse(paths); 384 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 385 } 386 387 /** 388 * Gets the content types the children of the given content can have. 389 * The result can contain 0, 1 or 2 content types 390 * @param refTableEntry The content 391 * @return the content types the children of the given content can have. 392 */ 393 public List<ContentType> getChildContentTypes(Content refTableEntry) 394 { 395 List<ContentType> result = new ArrayList<>(); 396 397 for (String cTypeId : refTableEntry.getTypes()) 398 { 399 ContentType cType = _contentTypeEP.getExtension(cTypeId); 400 if (_childByContentType.containsKey(cType)) 401 { 402 result.add(_childByContentType.get(cType)); 403 } 404 if (_autoReferencingContentTypes.contains(cType)) 405 { 406 result.add(cType); 407 } 408 409 if (!result.isEmpty()) 410 { 411 break; 412 } 413 } 414 415 return result; 416 } 417 418 /** 419 * Get the metadata values of a candidate 420 * @param contentId the id of candidate 421 * @return the candidate's values 422 */ 423 @Callable 424 public Map<String, Object> getCandidateValues(String contentId) 425 { 426 Map<String, Object> values = new HashMap<>(); 427 428 Content content = _resolver.resolveById(contentId); 429 values.put("title", content.getValue(Content.ATTRIBUTE_TITLE)); 430 values.put("comment", content.getValue("comment")); 431 432 return values; 433 } 434 435 /** 436 * Get the parent metadata 437 * @param contentId The content id 438 * @return the path of parent metadata or null if not found 439 */ 440 @Callable 441 public String getParentAttribute(String contentId) 442 { 443 Content content = _resolver.resolveById(contentId); 444 return getParentAttribute(content); 445 } 446 447 /** 448 * Get the parent metadata 449 * @param content The content 450 * @return the path of parent metadata or null if not found 451 */ 452 public String getParentAttribute(Content content) 453 { 454 for (String cTypeId : content.getTypes()) 455 { 456 ContentType cType = _contentTypeEP.getExtension(cTypeId); 457 Optional<ContentAttributeDefinition> parentMetadata = cType.getParentAttributeDefinition(); 458 if (!parentMetadata.isEmpty()) 459 { 460 return parentMetadata.get().getPath(); 461 } 462 } 463 464 return null; 465 } 466 467 /** 468 * Returns the "parent" attribute value for the given content, or null if it is not defined for its content types 469 * See also {@link ContentType#getParentAttributeDefinition()} 470 * @param content The content 471 * @return the "parent" attribute value for the given content, or null 472 */ 473 public String getParent(Content content) 474 { 475 for (String cTypeId : content.getTypes()) 476 { 477 if (!_contentTypeEP.hasExtension(cTypeId)) 478 { 479 getLogger().warn("The content {} ({}) has unknown type '{}'", content.getId(), content.getTitle(), cTypeId); 480 continue; 481 } 482 483 ContentType cType = _contentTypeEP.getExtension(cTypeId); 484 Optional<String> contentId = cType.getParentAttributeDefinition() 485 .map(ContentAttributeDefinition::getName) 486 .filter(name -> content.hasValue(name)) 487 .map(name -> content.getValue(name)) 488 .map(value -> ((ContentValue) value).getContentId()); 489 490 if (!contentId.isEmpty()) 491 { 492 return contentId.get(); 493 } 494 } 495 return null; 496 } 497 498 /** 499 * Recursively returns the "parent" metadata value for the given content, i.e; will return the parent, the parent of the parent, etc. 500 * @param content The content 501 * @return all the parents of the given content 502 */ 503 public List<String> getAllParents(Content content) 504 { 505 List<String> parents = new ArrayList<>(); 506 507 Content currentContent = content; 508 String parentId = getParent(currentContent); 509 510 while (parentId != null) 511 { 512 if (_resolver.hasAmetysObjectForId(parentId)) 513 { 514 parents.add(parentId); 515 currentContent = _resolver.resolveById(parentId); 516 parentId = getParent(currentContent); 517 } 518 else 519 { 520 break; 521 } 522 } 523 524 return parents; 525 } 526 527 /** 528 * Returns the direct children of a content 529 * @param content the content to get the direct children from 530 * @return the AmetysObjectIterable of the direct children of the content 531 */ 532 public AmetysObjectIterable<Content> getDirectChildren(Content content) 533 { 534 List<ContentType> childContentTypes = getChildContentTypes(content); 535 536 if (!childContentTypes.isEmpty()) 537 { 538 List<Expression> exprs = new ArrayList<>(); 539 for (ContentType childContentType : childContentTypes) 540 { 541 String pointingMetadataName = childContentType.getParentAttributeDefinition() 542 .map(ModelItem::getName) 543 .orElse(StringUtils.EMPTY); 544 545 if (StringUtils.isNotEmpty(pointingMetadataName)) 546 { 547 // //element(*, ametys:content)[@ametys-internal:contentType = 'foo.child.contentType' and @ametys:pointingMetadata = 'contentId'] 548 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, childContentType.getId()); 549 Expression parentExpr = new StringExpression(pointingMetadataName, Operator.EQ, content.getId()); 550 551 exprs.add(new AndExpression(cTypeExpr, parentExpr)); 552 } 553 } 554 555 Expression finalExpr = new OrExpression(exprs.toArray(new Expression[exprs.size()])); 556 557 SortCriteria sortCriteria = new SortCriteria(); 558 sortCriteria.addCriterion("title", true, true); 559 String xPathQuery = ContentQueryHelper.getContentXPathQuery(finalExpr, sortCriteria); 560 561 return _resolver.query(xPathQuery); 562 } 563 564 return new EmptyIterable<>(); 565 } 566 567 /** 568 * Returns a Set of all the descendants 569 * @param content the content to get the children from 570 * @return the Set of children 571 */ 572 public Set<String> getAllChildren(Content content) 573 { 574 AmetysObjectIterable<Content> directChildren = getDirectChildren(content); 575 Set<String> children = new HashSet<>(); 576 for (Content child : directChildren) 577 { 578 children.add(child.getId()); 579 children.addAll(getAllChildren(child)); 580 } 581 return children; 582 } 583 584 /** 585 * Return a boolean value to determine if all the contents are simple 586 * @param contentTypeLeaf the contentType id of the leaf content 587 * @return true if all the contents in the tree are simple, false otherwise 588 */ 589 @Callable 590 public boolean isHierarchicalSimpleTree(String contentTypeLeaf) 591 { 592 Set<String> cTypeIds = getHierarchicalContentTypes(contentTypeLeaf); 593 for (String cTypeId : cTypeIds) 594 { 595 ContentType cType = _contentTypeEP.getExtension(cTypeId); 596 if (cType != null && !cType.isSimple()) 597 { 598 return false; 599 } 600 } 601 602 return true; 603 } 604 605 /** 606 * Return the contents at the root 607 * @param rootContentType the content type of the contents at the root 608 * @return the contents at the root 609 */ 610 public AmetysObjectIterable<Content> getRootChildren(ContentType rootContentType) 611 { 612 return getRootChildren(rootContentType, false); 613 } 614 615 /** 616 * Return the contents at the root 617 * @param rootContentType the content type of the contents at the root 618 * @param excludeCandidates true to exclude candidates 619 * @return the contents at the root 620 */ 621 public AmetysObjectIterable<Content> getRootChildren(ContentType rootContentType, boolean excludeCandidates) 622 { 623 List<Expression> exprs = new ArrayList<>(); 624 625 exprs.add(new ContentTypeExpression(Operator.EQ, rootContentType.getId())); 626 627 // even if it is the top level type, parent attribute can be not null if it references itself as parent 628 rootContentType.getParentAttributeDefinition() 629 .filter(attribute -> rootContentType.getId().equals(attribute.getContentTypeId())) 630 .ifPresent(attribute -> exprs.add(new OrExpression( 631 new NotExpression(new MetadataExpression(attribute.getName())), // Search for contents without parent, 632 new StringExpression(attribute.getName(), Operator.EQ, StringUtils.EMPTY)))); // or with an empty parent 633 634 if (excludeCandidates) 635 { 636 exprs.add(new NotExpression(new MixinTypeExpression(Operator.EQ, CANDIDATE_CONTENT_TYPE))); 637 } 638 639 SortCriteria sort = new SortCriteria(); 640 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 641 642 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(exprs.toArray(new Expression[exprs.size()])), sort); 643 return _resolver.query(query); 644 } 645 646 /** 647 * Return the candidates for given content type 648 * @param cTypeId the id of content type 649 * @return the candidates 650 */ 651 public AmetysObjectIterable<Content> getCandidates(String cTypeId) 652 { 653 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, cTypeId); 654 Expression candidateExpr = new MixinTypeExpression(Operator.EQ, CANDIDATE_CONTENT_TYPE); 655 656 SortCriteria sort = new SortCriteria(); 657 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 658 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, candidateExpr), sort); 659 return _resolver.query(query); 660 } 661 662 /** 663 * Get the path of node which match filter regexp 664 * @param value the value to match 665 * @param contentId the content id from where we will filter 666 * @param leafContentType the leaf content type 667 * @return the matching paths 668 */ 669 @Callable 670 public List<String> filterReferenceTablesByRegExp (String value, String contentId, String leafContentType) 671 { 672 List<String> results = new ArrayList<>(); 673 ContentType topLevelContentType = getTopLevelType(_contentTypeEP.getExtension(leafContentType)); 674 try (AmetysObjectIterable<Content> contents = "root".equals(contentId) ? getRootChildren(topLevelContentType) : getDirectChildren((Content) _resolver.resolveById(contentId))) 675 { 676 for (Content content: contents) 677 { 678 _getMatchingPathsFromContent(content, value, results); 679 } 680 } 681 return results; 682 } 683 684 private void _getMatchingPathsFromContent(Content parent, String value, List<String> result) 685 { 686 if (_isMatchingContent(parent, value)) 687 { 688 result.add(getPathInHierarchy(parent.getId())); 689 } 690 691 AmetysObjectIterable<Content> directChildren = getDirectChildren(parent); 692 for (Content child : directChildren) 693 { 694 _getMatchingPathsFromContent(child, value, result); 695 } 696 } 697 698 /** 699 * <code>true</code> if the content match to the filter value 700 * @param content the content 701 * @param filterValue the filter value 702 * @return <code>true</code> if the content match to the filter value 703 */ 704 protected boolean _isMatchingContent(Content content, String filterValue) 705 { 706 String toMatch = org.ametys.core.util.StringUtils.normalizeStringValue(filterValue).trim(); 707 708 View view = Optional.ofNullable(_cTypeHelper.getView(SEARCH_FILTERS_VIEW_NAME, content)) // Get view of filter attributes for content 709 .orElseGet(() -> View.of(content.getModel(), Content.ATTRIBUTE_TITLE)); // Get a view with only title if search-filters view doesn't exist 710 711 return _getStringValuesToFilter(content, view) // Get the string values from the content to filter 712 .stream() 713 .anyMatch(v -> _isValueMatching(v, toMatch)); // Test if at least one value matches the filter value 714 } 715 716 /** 717 * Get all string value from the view. This method doesn't handle repeaters. 718 * @param dataHolder the data holder to get value 719 * @param viewItemAccessor the view item accessor where to search string values 720 * @return the list of string values 721 */ 722 protected List<String> _getStringValuesToFilter(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor) 723 { 724 List<String> stringValues = new ArrayList<>(); 725 726 ViewHelper.visitView(viewItemAccessor, 727 (element, definition) -> { 728 // simple element 729 String name = definition.getName(); 730 if (dataHolder.hasValue(name)) 731 { 732 // If an element is also a view item accessor, do not check the element itself but only its children 733 if (element instanceof ViewItemAccessor elementAccessor && !(elementAccessor.getViewItems().isEmpty())) 734 { 735 if (definition.isMultiple()) 736 { 737 ModelAwareDataHolder[] elementHolders = dataHolder.getValue(name); 738 for (ModelAwareDataHolder elementHolder : elementHolders) 739 { 740 stringValues.addAll(_getStringValuesToFilter(elementHolder, elementAccessor)); 741 } 742 } 743 else 744 { 745 ModelAwareDataHolder elementHolder = dataHolder.getValue(name); 746 stringValues.addAll(_getStringValuesToFilter(elementHolder, elementAccessor)); 747 } 748 } 749 else 750 { 751 if (definition.isMultiple()) 752 { 753 Arrays.stream((Object[]) dataHolder.getValue(name)) 754 .map(value -> definition.getType().toString(value)) 755 .forEach(stringValues::add); 756 } 757 else 758 { 759 Optional.ofNullable(dataHolder.getValue(name)) 760 .map(value -> definition.getType().toString(value)) 761 .ifPresent(stringValues::add); 762 } 763 } 764 } 765 }, 766 (group, definition) -> { 767 // composite 768 String name = definition.getName(); 769 ModelAwareComposite composite = dataHolder.getComposite(name); 770 if (composite != null) 771 { 772 stringValues.addAll(_getStringValuesToFilter(composite, group)); 773 } 774 }, 775 (group, definition) -> { 776 // repeater 777 String name = definition.getName(); 778 ModelAwareRepeater repeater = dataHolder.getRepeater(name); 779 if (repeater != null) 780 { 781 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 782 { 783 stringValues.addAll(_getStringValuesToFilter(entry, group)); 784 } 785 } 786 }, 787 group -> stringValues.addAll(_getStringValuesToFilter(dataHolder, group))); 788 789 return stringValues; 790 } 791 792 private boolean _isValueMatching(String value, String toMatch) 793 { 794 String normalizedValue = org.ametys.core.util.StringUtils.normalizeStringValue(value); 795 return normalizedValue.contains(toMatch); 796 } 797 798 /** 799 * Determines if this content type supports candidates 800 * @param contentType the content type 801 * @return true if the the candidates are allowed, false otherwise 802 */ 803 public boolean supportCandidates(ContentType contentType) 804 { 805 return contentType.hasTag(TAG_CANDIDATE); 806 } 807 808 /** 809 * Determines if this content type supports candidates 810 * @param contentTypeId the id of content type 811 * @return true if the the candidates are allowed, false otherwise 812 */ 813 @Callable 814 public boolean supportCandidates(String contentTypeId) 815 { 816 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 817 return supportCandidates(contentType); 818 } 819 820 /** 821 * Check if the content is a candidate 822 * @param content the content to test 823 * @return true if the content is a candidate, false otherwise 824 */ 825 public boolean isCandidate(Content content) 826 { 827 return ArrayUtils.contains(content.getMixinTypes(), CANDIDATE_CONTENT_TYPE); 828 } 829 830 /** 831 * Check if the referencing content is the parent of the content 832 * @param referencingContentId The referencing content if to check if it's the parent or not 833 * @param content the child content 834 * @return true if it's the parent, false otherwise 835 */ 836 protected boolean _isParent(String referencingContentId, Content content) 837 { 838 return Optional.of(content) 839 .map(this::getParentAttribute) 840 .map(name -> content.getValue(name)) 841 .map(ContentValue.class::cast) 842 .map(ContentValue::getContentId) 843 .map(parent -> referencingContentId.equals(parent)) 844 .orElse(false); 845 } 846 847 /** 848 * Test if content is referenced by other contents than its parent and children 849 * @param content The content to test 850 * @param contentsId The list of contents id to delete 851 * @return true if content is referenced 852 */ 853 protected boolean _isContentReferenced(Content content, List<String> contentsId) 854 { 855 return _isContentReferenced(content, contentsId, new HashSet<>()); 856 } 857 858 private boolean _isContentReferenced(Content content, List<String> contentsId, Set<String> alreadyVisited) 859 { 860 // If the content is not currently calculated or has already been calculated (avoid infinite loop) 861 if (alreadyVisited.add(content.getId())) 862 { 863 // For each referencing content 864 for (Content referencingContent : content.getReferencingContents()) 865 { 866 String referencingContentId = referencingContent.getId(); 867 868 // If parent, content is not really referenced 869 if (!_isParent(referencingContentId, content)) 870 { 871 // If the referencing content will be deleted or is a direct children of the current content (should be in the first list) 872 // Then control if the content is referenced in another way 873 if (contentsId.contains(referencingContentId) || _isParent(content.getId(), referencingContent)) 874 { 875 if (_isContentReferenced(referencingContent, contentsId, alreadyVisited)) 876 { 877 return true; 878 } 879 } 880 // The referencing content is not planned for deletion then the content is referenced 881 else 882 { 883 return true; 884 } 885 } 886 } 887 } 888 889 return false; 890 } 891}