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