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