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