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