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