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