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