001/*
002 *  Copyright 2014 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.contenttype;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.LinkedHashMap;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.Set;
032import java.util.TreeSet;
033import java.util.function.Predicate;
034
035import org.apache.avalon.framework.activity.Disposable;
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.configuration.ConfigurationException;
038import org.apache.avalon.framework.logger.AbstractLogEnabled;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.avalon.framework.thread.ThreadSafe;
043import org.apache.commons.collections.CollectionUtils;
044import org.apache.commons.lang3.ArrayUtils;
045import org.apache.commons.lang3.StringUtils;
046
047import org.ametys.cms.content.RootContentHelper;
048import org.ametys.cms.contenttype.indexing.IndexingField;
049import org.ametys.cms.contenttype.indexing.IndexingModel;
050import org.ametys.cms.repository.Content;
051import org.ametys.core.right.RightManager;
052import org.ametys.core.right.RightManager.RightResult;
053import org.ametys.core.ui.Callable;
054import org.ametys.core.user.CurrentUserProvider;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.plugins.repository.AmetysRepositoryException;
057import org.ametys.runtime.i18n.I18nizableText;
058
059/**
060 * Helper for manipulating {@link ContentType}s
061 * 
062 */
063public class ContentTypesHelper extends AbstractLogEnabled implements Component, Serviceable, ThreadSafe, Disposable
064{
065    /** The Avalon role */
066    public static final String ROLE = ContentTypesHelper.class.getName();
067
068    /** The content types extension point */
069    protected ContentTypeExtensionPoint _cTypeEP;
070    /** The current user provider */
071    protected CurrentUserProvider _userProvider;
072    /** The rights manager */
073    protected RightManager _rightManager;
074    /** Helper for root content */
075    protected RootContentHelper _rootContentHelper;
076    
077    private DynamicContentTypeDescriptorExtentionPoint _dynamicCTDescriptorEP;
078
079    // Cache
080    private Map<String, Map<String, MetadataSet>> _cacheForView = new HashMap<>();
081    private Map<String, Map<String, MetadataSet>> _cacheForEdition = new HashMap<>();
082
083    private ServiceManager _smanager;
084
085    @Override
086    public void service(ServiceManager smanager) throws ServiceException
087    {
088        _smanager = smanager;
089        _dynamicCTDescriptorEP = (DynamicContentTypeDescriptorExtentionPoint) smanager.lookup(DynamicContentTypeDescriptorExtentionPoint.ROLE);
090        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
091        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
092        _rootContentHelper = (RootContentHelper) smanager.lookup(RootContentHelper.ROLE);
093    }
094
095    @Override
096    public void dispose()
097    {
098        _cacheForView = new HashMap<>();
099        _cacheForEdition = new HashMap<>();
100    }
101    
102    /**
103     * Lazy lookup of {@link ContentTypeExtensionPoint}
104     * @return the content type extension point
105     */
106    protected ContentTypeExtensionPoint _getContentTypeEP()
107    {
108        if (_cTypeEP == null)
109        {
110            try
111            {
112                _cTypeEP = (ContentTypeExtensionPoint) _smanager.lookup(ContentTypeExtensionPoint.ROLE);
113            }
114            catch (ServiceException e)
115            {
116                throw new RuntimeException("Unable to lookup ContentTypeExtensionPoint component", e);
117            }
118        }
119        return _cTypeEP;
120    }
121
122    /**
123     * Determines if a content is a instance of given content type id
124     * 
125     * @param content The content
126     * @param cTypeId The id of content type or mixin
127     * @return <code>true</code> if the content is an instance of content type
128     */
129    public boolean isInstanceOf(Content content, String cTypeId)
130    {
131        String[] types = content.getTypes();
132        if (ArrayUtils.contains(types, cTypeId))
133        {
134            return true;
135        }
136
137        String[] mixins = content.getMixinTypes();
138        if (ArrayUtils.contains(mixins, cTypeId))
139        {
140            return true;
141        }
142
143        return _containsContentType(ArrayUtils.addAll(types, mixins), cTypeId);
144    }
145
146    private boolean _containsContentType(String[] cTypesId, String cTypeId)
147    {
148        for (String id : cTypesId)
149        {
150            ContentType cType = _getContentTypeEP().getExtension(id);
151            if (cType != null)
152            {
153                if (ArrayUtils.contains(cType.getSupertypeIds(), cTypeId))
154                {
155                    return true;
156                }
157                else if (_containsContentType(cType.getSupertypeIds(), cTypeId))
158                {
159                    return true;
160                }
161            }
162        }
163        return false;
164    }
165
166    /**
167     * Get the id of the content type common ancestor
168     * 
169     * @param contentTypes The content types to compare
170     * @return The id of common ancestor or <code>null</code> if not found
171     */
172    public String getCommonAncestor(Collection<String> contentTypes)
173    {
174        if (contentTypes.isEmpty())
175        {
176            return null;
177        }
178
179        if (contentTypes.size() == 1)
180        {
181            return contentTypes.iterator().next();
182        }
183
184        List<Collection<String>> superTypesByCType = new ArrayList<>();
185        for (String id : contentTypes)
186        {
187            Set<String> superTypes = new HashSet<>();
188
189            superTypes.add(id);
190            superTypes.addAll(getAncestors(id));
191
192            superTypesByCType.add(superTypes);
193        }
194        
195        Iterator<Collection<String>> superTypesByCTypeIt = superTypesByCType.iterator();
196        
197        Set<String> commonCTypes = new HashSet<>(superTypesByCTypeIt.next());
198        while (superTypesByCTypeIt.hasNext() && !commonCTypes.isEmpty())
199        {
200            commonCTypes.retainAll(superTypesByCTypeIt.next());
201        }
202        
203        commonCTypes = removeAncestors(commonCTypes);
204        
205        if (commonCTypes.size() == 1)
206        {
207            return commonCTypes.iterator().next();
208        }
209
210        return null; // No common ancestor
211    }
212
213    /**
214     * Remove all content types in the set that are ancestors of other content types in the set.
215     * @param contentTypes a Set of content type IDs.
216     * @return a Set of content type IDs without ancestors.
217     */
218    protected Set<String> removeAncestors(Set<String> contentTypes)
219    {
220        Set<String> noAncestors = new HashSet<>(contentTypes);
221        
222        Iterator<String> it1 = contentTypes.iterator();
223        while (it1.hasNext())
224        {
225            ContentType cType1 = _getContentTypeEP().getExtension(it1.next());
226            
227            Iterator<String> it2 = contentTypes.iterator();
228            while (it2.hasNext())
229            {
230                String cType2 = it2.next();
231                String[] supertypeIds = cType1.getSupertypeIds();
232                
233                // CType2 is an ancestor of CType1: remove it.
234                if (ArrayUtils.contains(supertypeIds, cType2))
235                {
236                    noAncestors.remove(cType2);
237                }
238            }
239        }
240        
241        return noAncestors;
242    }
243
244    /**
245     * Get all ancestors for the given content type
246     * 
247     * @param contentTypeId The content type id to test
248     * @return A non-null set of all ancestors. Does not contains the contentTypeId itself.
249     * @throws IllegalArgumentException if the content type does not exist.
250     */
251    public Set<String> getAncestors(String contentTypeId)
252    {
253        Set<String> superTypes = new HashSet<>();
254
255        ContentType cType = _getContentTypeEP().getExtension(contentTypeId);
256        
257        if (cType == null)
258        {
259            throw new IllegalArgumentException("Unable to get anscestors of unknown content type '" + contentTypeId + "'");
260        }
261        
262
263        String[] supertypeIds = cType.getSupertypeIds();
264
265        for (String superTypeId : supertypeIds)
266        {
267            superTypes.add(superTypeId);
268            superTypes.addAll(getAncestors(superTypeId));
269        }
270
271        return superTypes;
272
273    }
274    
275    /**
276     * Builds the reverse hierarchies of ancestors of a content type
277     * @param contentTypeId The content type's id
278     * @return the reverse hierarchies with ancestors
279     */
280    public List<Set<String>> buildReverseHierarchies(String contentTypeId)
281    {
282        Set<String> hierarchy = new LinkedHashSet<>();
283        hierarchy.add(contentTypeId);
284        return _buildReverseHierarchies (contentTypeId, hierarchy);
285    }
286    
287    private List<Set<String>> _buildReverseHierarchies(String contentTypeId, Set<String> hierarchy)
288    {
289        List<Set<String>> hierarchies = new ArrayList<>();
290        
291        ContentType cType = _getContentTypeEP().getExtension(contentTypeId);
292        if (cType == null)
293        {
294            throw new IllegalArgumentException("Unable to get anscestors of unknown content type '" + contentTypeId + "'");
295        }
296        
297        String[] supertypeIds = cType.getSupertypeIds();
298        if (supertypeIds.length > 0)
299        {
300            for (String superTypeId : supertypeIds)
301            {
302                Set<String> superHierarchy = new LinkedHashSet<>(hierarchy);
303                superHierarchy.add(superTypeId);
304                hierarchies.addAll(_buildReverseHierarchies(superTypeId, superHierarchy));
305            }
306        }
307        else
308        {
309            hierarchies.add(hierarchy);
310        }
311        
312        return hierarchies;
313    }
314    
315    /**
316     * Retrieves the root metadata names of a content.
317     * @param content The content.
318     * @return the metadata names.
319     */
320    public Set<String> getMetadataNames(Content content)
321    {
322        return getMetadataNames(content.getTypes(), content.getMixinTypes());
323    }
324    
325    /**
326     * Retrieves the metadata names resulting of the union of metadata
327     * names of given content types and mixins
328     * 
329     * @param cTypes The id of content types
330     * @param mixins The id of mixins
331     * @return the metadata names.
332     */
333    public Set<String> getMetadataNames(String[] cTypes, String[] mixins)
334    {
335        Set<String> metadataNames = new HashSet<>();
336
337        for (String id : cTypes)
338        {
339            ContentType cType = _getContentTypeEP().getExtension(id);
340            metadataNames.addAll(cType.getMetadataNames());
341        }
342
343        for (String id : mixins)
344        {
345            ContentType cType = _getContentTypeEP().getExtension(id);
346            metadataNames.addAll(cType.getMetadataNames());
347        }
348
349        return metadataNames;
350    }
351
352    /**
353     * Get all metadata sets for view resulting of the concatenation of metadata
354     * sets of given content types and mixins.
355     * 
356     * @param cTypes The id of content types
357     * @param mixins The id of mixins
358     * @param includeInternal <code>true</code> True to include internal
359     *            metadata sets
360     * @return The metadata sets
361     */
362    public Map<String, MetadataSet> getMetadataSetsForView(String[] cTypes, String[] mixins, boolean includeInternal)
363    {
364        Map<String, MetadataSet> metadataSets = new HashMap<>();
365
366        Set<String> viewMetadataSetNames = new HashSet<>();
367        for (String id : cTypes)
368        {
369            ContentType cType = _getContentTypeEP().getExtension(id);
370            for (String name : cType.getViewMetadataSetNames(includeInternal))
371            {
372                if (!viewMetadataSetNames.contains(name))
373                {
374                    viewMetadataSetNames.add(name);
375                }
376            }
377        }
378
379        for (String viewMetadataSetName : viewMetadataSetNames)
380        {
381            metadataSets.put(viewMetadataSetName, getMetadataSetForView(viewMetadataSetName, cTypes, mixins));
382        }
383
384        return metadataSets;
385    }
386
387    /**
388     * Get all metadata sets for edition resulting of the concatenation of
389     * metadata sets of given content types and mixins.
390     * 
391     * @param cTypes The id of content types
392     * @param mixins The id of mixins
393     * @param includeInternal <code>true</code> True to include internal
394     *            metadata sets
395     * @return The metadata sets
396     */
397    public Map<String, MetadataSet> getMetadataSetsForEdition(String[] cTypes, String[] mixins, boolean includeInternal)
398    {
399        Map<String, MetadataSet> metadataSets = new HashMap<>();
400
401        Set<String> editionMetadataSetNames = new HashSet<>();
402        for (String id : cTypes)
403        {
404            ContentType cType = _getContentTypeEP().getExtension(id);
405            for (String name : cType.getEditionMetadataSetNames(includeInternal))
406            {
407                if (!editionMetadataSetNames.contains(name))
408                {
409                    editionMetadataSetNames.add(name);
410                }
411            }
412        }
413
414        for (String editionMetadataSetName : editionMetadataSetNames)
415        {
416            metadataSets.put(editionMetadataSetName, getMetadataSetForEdition(editionMetadataSetName, cTypes, mixins));
417        }
418
419        return metadataSets;
420    }
421
422    /**
423     * Get the metadata set for view resulting of the concatenation of metadata
424     * sets of given content types and mixins.
425     * 
426     * @param metadataSetName the name of metadata set to retrieve
427     * @param cTypes The id of content types
428     * @param mixins The id of mixins
429     * @return The list of metadata set names.
430     */
431    public MetadataSet getMetadataSetForView(String metadataSetName, String[] cTypes, String[] mixins)
432    {
433        String cacheId = _getCacheIdentifier(cTypes, mixins);
434
435        if (_cacheForView.containsKey(cacheId) && _cacheForView.get(cacheId).containsKey(metadataSetName))
436        {
437            return _cacheForView.get(cacheId).get(metadataSetName);
438        }
439
440        MetadataSet joinMetadataSet = new MetadataSet();
441        boolean foundOne = false;
442
443        for (String id : cTypes)
444        {
445            ContentType cType = _getContentTypeEP().getExtension(id);
446
447            MetadataSet metadataSet = cType.getMetadataSetForView(metadataSetName);
448            if (metadataSet != null)
449            {
450                foundOne = true;
451                copyMetadataSetElementsIfNotExist(metadataSet, joinMetadataSet);
452
453                if (joinMetadataSet.getName() == null)
454                {
455                    joinMetadataSet.setName(metadataSetName);
456                    joinMetadataSet.setLabel(metadataSet.getLabel());
457                    joinMetadataSet.setDescription(metadataSet.getDescription());
458                    joinMetadataSet.setIconGlyph(metadataSet.getIconGlyph());
459                    joinMetadataSet.setIconDecorator(metadataSet.getIconDecorator());
460                    joinMetadataSet.setSmallIcon(metadataSet.getSmallIcon());
461                    joinMetadataSet.setMediumIcon(metadataSet.getMediumIcon());
462                    joinMetadataSet.setLargeIcon(metadataSet.getLargeIcon());
463                    joinMetadataSet.setEdition(false);
464                }
465            }
466        }
467
468        for (String id : mixins)
469        {
470            ContentType mixin = _getContentTypeEP().getExtension(id);
471
472            MetadataSet metadataSet = mixin.getMetadataSetForView(metadataSetName);
473            if (metadataSet != null)
474            {
475                foundOne = true;
476                copyMetadataSetElementsIfNotExist(metadataSet, joinMetadataSet);
477            }
478        }
479
480        if (!foundOne)
481        {
482            return null;
483        }
484        
485        if (!_cacheForView.containsKey(cacheId))
486        {
487            _cacheForView.put(cacheId, new HashMap<String, MetadataSet>());
488        }
489        _cacheForView.get(cacheId).put(metadataSetName, joinMetadataSet);
490
491        return joinMetadataSet;
492    }
493
494    /**
495     * Get the metadata set for edition resulting of the concatenation of
496     * metadata sets of given content types and mixins.
497     * 
498     * @param metadataSetName the name of metadata set to retrieve
499     * @param cTypes The id of content types
500     * @param mixins The id of mixins
501     * @return The metadata set
502     */
503    public MetadataSet getMetadataSetForEdition(String metadataSetName, String[] cTypes, String[] mixins)
504    {
505        String cacheId = _getCacheIdentifier(cTypes, mixins);
506
507        if (_cacheForEdition.containsKey(cacheId) && _cacheForEdition.get(cacheId).containsKey(metadataSetName))
508        {
509            return _cacheForEdition.get(cacheId).get(metadataSetName);
510        }
511
512        MetadataSet joinMetadataSet = new MetadataSet();
513        boolean foundOne = false;
514
515        for (String id : cTypes)
516        {
517            ContentType cType = _getContentTypeEP().getExtension(id);
518
519            MetadataSet metadataSet = cType.getMetadataSetForEdition(metadataSetName);
520            if (metadataSet != null)
521            {
522                foundOne = true;
523                copyMetadataSetElementsIfNotExist(metadataSet, joinMetadataSet);
524
525                if (joinMetadataSet.getName() == null)
526                {
527                    joinMetadataSet.setName(metadataSetName);
528                    joinMetadataSet.setLabel(metadataSet.getLabel());
529                    joinMetadataSet.setDescription(metadataSet.getDescription());
530                    joinMetadataSet.setIconGlyph(metadataSet.getIconGlyph());
531                    joinMetadataSet.setIconDecorator(metadataSet.getIconDecorator());
532                    joinMetadataSet.setSmallIcon(metadataSet.getSmallIcon());
533                    joinMetadataSet.setMediumIcon(metadataSet.getMediumIcon());
534                    joinMetadataSet.setLargeIcon(metadataSet.getLargeIcon());
535                    joinMetadataSet.setEdition(true);
536                }
537            }
538        }
539
540        for (String id : mixins)
541        {
542            ContentType mixin = _getContentTypeEP().getExtension(id);
543
544            MetadataSet metadataSet = mixin.getMetadataSetForEdition(metadataSetName);
545            if (metadataSet != null)
546            {
547                foundOne = true;
548                copyMetadataSetElementsIfNotExist(metadataSet, joinMetadataSet);
549            }
550        }
551
552        if (!foundOne)
553        {
554            return null;
555        }
556        
557        if (!_cacheForEdition.containsKey(cacheId))
558        {
559            _cacheForEdition.put(cacheId, new HashMap<String, MetadataSet>());
560        }
561        _cacheForEdition.get(cacheId).put(metadataSetName, joinMetadataSet);
562
563        return joinMetadataSet;
564    }
565    
566    /**
567     * Get the metadata sets of a content type
568     * @param cTypeId the content type id
569     * @param edition Set to true to get edition metadata set. False otherwise.
570     * @param includeInternal Set to true to include internal metadata sets.
571     * @return the metadata sets info
572     */
573    @Callable
574    public List<Map<String, Object>> getMetadataSetsInfo(String cTypeId, boolean edition, boolean includeInternal)
575    {
576        List<Map<String, Object>> metadataSets = new ArrayList<>();
577        
578        ContentType cType = _getContentTypeEP().getExtension(cTypeId);
579        
580        Set<String> metadataSetNames = edition ? cType.getEditionMetadataSetNames(includeInternal) : cType.getViewMetadataSetNames(includeInternal);
581        for (String metadataSetName : metadataSetNames)
582        {
583            MetadataSet metadataSet = edition ? cType.getMetadataSetForEdition(metadataSetName) : cType.getMetadataSetForView(metadataSetName);
584            
585            Map<String, Object> viewInfos = new HashMap<>();
586            viewInfos.put("name", metadataSetName);
587            viewInfos.put("label", metadataSet.getLabel());
588            viewInfos.put("description", metadataSet.getDescription());
589            metadataSets.add(viewInfos);
590        }
591        
592        return metadataSets;
593    }
594    
595    /**
596     * Get the indexing model resulting of the concatenation of indexing models of given content types and mixins.
597     * @param content The content.
598     * @return The indexing model
599     */
600    public IndexingModel getIndexingModel(Content content)
601    {
602        return getIndexingModel(content.getTypes(), content.getMixinTypes());
603    }
604
605    /**
606     * Get the indexing model resulting of the concatenation of indexing models of given content types and mixins.
607     * @param cTypes The id of content types
608     * @param mixins The id of mixins
609     * @return The indexing model
610     */
611    public IndexingModel getIndexingModel(String[] cTypes, String[] mixins)
612    {
613        IndexingModel joinIndexingModel = new IndexingModel();
614        
615        for (String id : cTypes)
616        {
617            ContentType cType = _getContentTypeEP().getExtension(id);
618
619            if (cType != null)
620            {
621                IndexingModel indexingModel = cType.getIndexingModel();
622                
623                for (IndexingField field : indexingModel.getFields())
624                {
625                    joinIndexingModel.addIndexingField(field);
626                }
627            }
628            else
629            {
630                if (getLogger().isWarnEnabled())
631                {
632                    getLogger().warn(String.format("Trying to get indexing model for an unknown content type : '%s'.", id));
633                }
634            }
635        }
636        
637        for (String id : mixins)
638        {
639            ContentType mixin = _getContentTypeEP().getExtension(id);
640            
641            if (mixin != null)
642            {
643                IndexingModel indexingModel = mixin.getIndexingModel();
644                
645                for (IndexingField field : indexingModel.getFields())
646                {
647                    joinIndexingModel.addIndexingField(field);
648                }
649            }
650            else
651            {
652                if (getLogger().isWarnEnabled())
653                {
654                    getLogger().warn(String.format("Trying to get indexing model for an unknown mixin type : '%s'.", id));
655                }
656            }
657        }
658        
659        return joinIndexingModel;
660    }
661    
662    /**
663     * Copy the elements of metadata set into a metadata set of destination,
664     * only if the elements are not already presents
665     * 
666     * @param src The metadata set to copy
667     * @param dest The metadata of destination
668     */
669    public void copyMetadataSetElementsIfNotExist(AbstractMetadataSetElement src, AbstractMetadataSetElement dest)
670    {
671        _copyMetadataSetElementsIfNotExist(src, dest, null);
672    }
673
674    private void _copyMetadataSetElementsIfNotExist(AbstractMetadataSetElement src, AbstractMetadataSetElement dest, AbstractMetadataSetElement reference)
675    {
676        AbstractMetadataSetElement root = reference == null ? dest : reference;
677
678        for (AbstractMetadataSetElement elmt : src.getElements())
679        {
680            if (elmt instanceof MetadataDefinitionReference)
681            {
682                String metadataName = ((MetadataDefinitionReference) elmt).getMetadataName();
683                if (!root.hasMetadataDefinitionReference(metadataName))
684                {
685                    dest.addElement(elmt);
686                }
687            }
688            else if (elmt instanceof Fieldset)
689            {
690                Fieldset fieldset = new Fieldset();
691                fieldset.setLabel(((Fieldset) elmt).getLabel());
692                fieldset.setRole(((Fieldset) elmt).getRole());
693
694                _copyMetadataSetElementsIfNotExist(elmt, fieldset, root);
695
696                if (fieldset.getElements().size() > 0)
697                {
698                    dest.addElement(fieldset);
699                }
700            }
701        }
702    }
703    
704    /**
705     * Retrieve the definition of a given content metadata value path.
706     * @param metadataValuePath the metadata value path: it is separated by slashes and repeaters are valued: eg: "mycomposite/myrepeater[3]/mymetadata".
707     * @param content the content.
708     * @return the metadata definition or <code>null</code> if not found
709     */
710    public MetadataDefinition getMetadataDefinitionByMetadataValuePath(String metadataValuePath, Content content)
711    {
712        return getMetadataDefinitionByMetadataValuePath(metadataValuePath, content.getTypes(), content.getMixinTypes());
713    }
714
715    /**
716     * Retrieve the definition of a given content metadata value path.
717     * @param metadataValuePath the metadata value path: it is separated by slashes and repeaters are valued: eg: "mycomposite.myrepeater[3].mymetadata".
718     * @param cTypes The id of content types
719     * @param mixins The id of mixins
720     * @return the metadata definition or <code>null</code> if not found
721     */
722    public MetadataDefinition getMetadataDefinitionByMetadataValuePath(String metadataValuePath, String[] cTypes, String[] mixins)
723    {
724        for (String id : cTypes)
725        {
726            ContentType cType = _getContentTypeEP().getExtension(id);
727
728            MetadataDefinition metadataDef = getMetadataDefinitionByMetadataValuePath(metadataValuePath, cType);
729            if (metadataDef != null)
730            {
731                return metadataDef;
732            }
733        }
734
735        for (String id : mixins)
736        {
737            ContentType cType = _getContentTypeEP().getExtension(id);
738
739            MetadataDefinition metadataDef = getMetadataDefinitionByMetadataValuePath(metadataValuePath, cType);
740            if (metadataDef != null)
741            {
742                return metadataDef;
743            }
744        }
745
746        return null;
747    }
748    
749    /**
750     * Retrieve the definition of a given content metadata value path.
751     * @param metadataValuePath the metadata value path: it is separated by slashes and repeaters are valued: eg: "mycomposite/myrepeater[3]/mymetadata".
752     * @param metadataDefHolder The metadata def holder (such as the content type)
753     * @return the metadata definition or <code>null</code> if not found
754     */
755    private MetadataDefinition getMetadataDefinitionByMetadataValuePath(String metadataValuePath, MetadataDefinitionHolder metadataDefHolder)
756    {
757        String metadataName = StringUtils.substringBefore(metadataValuePath, ContentConstants.METADATA_PATH_SEPARATOR);
758        String subMetadataPath = StringUtils.substringAfter(metadataValuePath, ContentConstants.METADATA_PATH_SEPARATOR);
759        // Ignore the repeater entry if any (we're only browsing the model).
760        metadataName = StringUtils.substringBefore(metadataName, "[");
761        
762        MetadataDefinition metadataDefinition = metadataDefHolder.getMetadataDefinition(metadataName);
763        if (metadataDefinition != null && StringUtils.isNotBlank(subMetadataPath))
764        {
765            if (metadataDefinition.getType() == MetadataType.COMPOSITE)
766            {
767                return getMetadataDefinitionByMetadataValuePath(subMetadataPath, metadataDefinition);
768            }
769            else
770            {
771                return null;
772            }
773        }
774        
775        return metadataDefinition;
776    }
777    
778    /**
779     * Get a flat map of MetadataDefinition corresponding to a metadataset of a content
780     * @param metadataSet The metadataset to get
781     * @return The flat map of metadatapath and MetadataDefinition
782     */
783    public Set<String> getMetadataPaths(MetadataSet metadataSet)
784    {
785        Set<String> results = new HashSet<>();
786
787        for (AbstractMetadataSetElement subMetadataSetElement : metadataSet.getElements())
788        {
789            results.addAll(getMetadataPaths(subMetadataSetElement, ""));
790        }
791        
792        return results;
793    }
794    
795    /**
796     * Get a flat map of metadatadefinition corresponding to a metadataset of a content
797     * @param metadataSet The metadataset to get
798     * @param prefix The metadata path of the parent
799     * @return The flat map of metadatapath and metadatadefinition
800     */
801    protected Set<String> getMetadataPaths(AbstractMetadataSetElement metadataSet, String prefix)
802    {
803        Set<String> results = new HashSet<>();
804        
805        MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) metadataSet;
806        
807        String metadataName = metadataDefRef.getMetadataName();
808        
809        String metadataPath = prefix + metadataName;
810        results.add(metadataPath);
811        
812        for (AbstractMetadataSetElement subMetadataSetElement : metadataSet.getElements())
813        {
814            results.addAll(getMetadataPaths(subMetadataSetElement, metadataPath + ContentConstants.METADATA_PATH_SEPARATOR));
815        }
816        
817        return results;
818    }    
819    
820    /** 
821     * Retrieve the metadata definition represented by the given path.
822     * The path can represent a metadata on another content.
823     * @param metadataPath the metadata path separated by '/'
824     * @param content The content.
825     * @return the metadata definition or null.
826     */ 
827    public MetadataDefinition getMetadataDefinition(String metadataPath, Content content)
828    {
829        List<MetadataDefinition> metadataDefinitionsByPath = getMetadataDefinitionPath(metadataPath, content);
830        return metadataDefinitionsByPath.size() > 0 ? metadataDefinitionsByPath.get(metadataDefinitionsByPath.size() - 1) : null;
831    }
832
833    /**
834     * Get a flat map of MetadataDefinition corresponding to a set of metadata paths
835     * @param metadataPaths A set of metadata path to analyse
836     * @param content The content associated
837     * @return The flat map of metadatapath and MetadataDefinition
838     */
839    public Map<String, MetadataDefinition> getMetadataDefinitions(Set<String> metadataPaths, Content content)
840    {
841        Map<String, MetadataDefinition> results = new HashMap<>();
842        
843        for (String metadataPath : metadataPaths)
844        {
845            results.put(metadataPath, getMetadataDefinition(metadataPath, content));
846        }
847        
848        return results;
849    }
850    
851    /**
852     * Get a flat map of MetadataDefinition corresponding to a metadataSet
853     * @param metadataSet A metadataSet to analyse
854     * @param content The content associated
855     * @return The flat map of metadatapath and MetadataDefinition
856     */
857    public Map<String, MetadataDefinition> getMetadataDefinitions(MetadataSet metadataSet, Content content)
858    {
859        return getMetadataDefinitions(getMetadataPaths(metadataSet), content);
860    }
861    
862    /** 
863     * Retrieve the list of successive metadata definitions represented by the given path.
864     * The path can represent a metadata on another content.
865     * @param metadataPath the metadata path separated by '/'
866     * @param content The content.
867     * @return the list of metadata definitions, one by path element.
868     */ 
869    public List<MetadataDefinition> getMetadataDefinitionPath(String metadataPath, Content content)
870    {
871        return getMetadataDefinitionPath(metadataPath, content.getTypes(), content.getMixinTypes());
872    }
873    
874    /**
875     * Get a flat map of MetadataDefinition path corresponding to a set of metadata paths
876     * @param metadataPaths A set of metadata path to analyse
877     * @param content The content associated
878     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
879     */
880    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(Set<String> metadataPaths, Content content)
881    {
882        Map<String, List<MetadataDefinition>> results = new HashMap<>();
883        
884        for (String metadataPath : metadataPaths)
885        {
886            results.put(metadataPath, getMetadataDefinitionPath(metadataPath, content));
887        }
888        
889        return results;
890    }
891    
892    /**
893     * Get a flat map of MetadataDefinition path corresponding to a metadataset
894     * @param metadataSet A metadataset to analyse
895     * @param content The content associated
896     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
897     */
898    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(MetadataSet metadataSet, Content content)
899    {
900        return getMetadataDefinitionsPaths(getMetadataPaths(metadataSet), content);
901    }
902    
903    /** 
904     * Retrieve the metadata definition represented by the given path.
905     * The path can represent a metadata on another content.
906     * @param metadataPath the metadata path separated by '/'
907     * @param cTypes The content types.
908     * @param mixins The content mixins types.
909     * @return the metadata definition or null.
910     */ 
911    public MetadataDefinition getMetadataDefinition(String metadataPath, String[] cTypes, String[] mixins)
912    {
913        List<MetadataDefinition> metadataDefinitionsByPath = getMetadataDefinitionPath(metadataPath, cTypes, mixins);
914        return metadataDefinitionsByPath.size() > 0 ? metadataDefinitionsByPath.get(metadataDefinitionsByPath.size() - 1) : null;
915    }
916
917    /** 
918     * Get a flat map of MetadataDefinition corresponding to a set of metadata paths
919     * @param metadataPaths A set of metadata path to analyse
920     * @param cTypes The content types.
921     * @param mixins The content mixins types.
922     * @return The flat map of metadatapath and MetadataDefinition
923     */ 
924    public Map<String, MetadataDefinition> getMetadataDefinitions(Set<String> metadataPaths, String[] cTypes, String[] mixins)
925    {
926        Map<String, MetadataDefinition> results = new HashMap<>();
927        
928        for (String metadataPath : metadataPaths)
929        {
930            results.put(metadataPath, getMetadataDefinition(metadataPath, cTypes, mixins));
931        }
932        
933        return results;
934    }
935    
936    /** 
937     * Get a flat map of MetadataDefinition corresponding to a metadataset
938     * @param metadataSet A metadataset to analyse
939     * @param cTypes The content types.
940     * @param mixins The content mixins types.
941     * @return The flat map of metadatapath and MetadataDefinition
942     */ 
943    public Map<String, MetadataDefinition> getMetadataDefinitions(MetadataSet metadataSet, String[] cTypes, String[] mixins)
944    {
945        return getMetadataDefinitions(getMetadataPaths(metadataSet), cTypes, mixins);
946    }
947    
948    /**
949     * Get a flat map of MetadataDefinition corresponding to the required predicate (boolean-valued function)
950     *  For example, to get all metadata definitions of type CONTENT, we can do:
951     *      getMetadataDefinitions(contentType, m -&gt; m.getType() == MetadataType.CONTENT)
952     *      
953     * @param metaDefHolder The metadata definition holder
954     * @param predicate The predicate (ex. m -&gt; m.getType() == MetadataType.CONTENT)
955     * @return  The flat map of metadata's path and their definition
956     */
957    public Map<String, MetadataDefinition> getMetadataDefinitions(MetadataDefinitionHolder metaDefHolder, Predicate<MetadataDefinition> predicate)
958    {
959        Map<String, MetadataDefinition> metaDefs = new HashMap<>();
960        
961        Set<String> metadataNames = metaDefHolder.getMetadataNames();
962        for (String metadataName : metadataNames)
963        {
964            MetadataDefinition metadataDef = metaDefHolder.getMetadataDefinition(metadataName);
965            if (predicate.test(metadataDef))
966            {
967                metaDefs.put(metadataDef.getId(), metadataDef);
968            }
969            
970            if (metadataDef.getType() == MetadataType.COMPOSITE || metadataDef instanceof RepeaterDefinition)
971            {
972                metaDefs.putAll(getMetadataDefinitions(metadataDef, predicate));
973            }
974        }
975        
976        return metaDefs;
977    }
978    
979    /** 
980     * Retrieve the list of successive metadata definitions represented by the given path.
981     * The path can represent a metadata on another content.
982     * @param metadataPath the metadata path separated by '/'
983     * @param cTypes The id of content types
984     * @param mixins The id of mixins
985     * @return the metadata definition or <code>null</code> if not found
986     */
987    public List<MetadataDefinition> getMetadataDefinitionPath(String metadataPath, String[] cTypes, String[] mixins)
988    {
989        String[] allContentTypes = ArrayUtils.addAll(cTypes, mixins);
990        
991        for (String cTypeId : allContentTypes)
992        {
993            ContentType cType = _getContentTypeEP().getExtension(cTypeId);
994            if (cType != null)
995            {
996                List<MetadataDefinition> metaDefs = getMetadataDefinitionPath(metadataPath, cType);
997                if (!metaDefs.isEmpty())
998                { 
999                    return metaDefs;
1000                }
1001            }
1002            else
1003            {
1004                if (getLogger().isWarnEnabled())
1005                {
1006                    getLogger().warn("Unknown content type identifier : " + cTypeId);
1007                }
1008            }
1009        }
1010        
1011        return Collections.emptyList();
1012    }
1013    
1014    /**
1015     * Get a flat map of MetadataDefinition path corresponding to a set of metadata paths
1016     * @param metadataPaths A set of metadata path to analyse
1017     * @param cTypes The id of content types
1018     * @param mixins The id of mixins
1019     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
1020     */
1021    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(Set<String> metadataPaths, String[] cTypes, String[] mixins)
1022    {
1023        Map<String, List<MetadataDefinition>> results = new HashMap<>();
1024        
1025        for (String metadataPath : metadataPaths)
1026        {
1027            results.put(metadataPath, getMetadataDefinitionPath(metadataPath, cTypes, mixins));
1028        }
1029        
1030        return results;
1031    }
1032    
1033    /**
1034     * Get a flat map of MetadataDefinition path corresponding to a metadataset
1035     * @param metadataSet A metadataset to analyse
1036     * @param cTypes The id of content types
1037     * @param mixins The id of mixins
1038     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
1039     */
1040    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(MetadataSet metadataSet, String[] cTypes, String[] mixins)
1041    {
1042        return getMetadataDefinitionsPaths(getMetadataPaths(metadataSet), cTypes, mixins);
1043    }
1044    
1045    /** 
1046     * Retrieves a metadata definition from a path. The metadata can be defined in a referenced or sub content.
1047     * @param metadataPath the metadata path separated by '/'
1048     * @param initialContentType The initial content type to start the search
1049     * @return the metadata definition or <code>null</code> if not found 
1050     */ 
1051    public MetadataDefinition getMetadataDefinition(String metadataPath, ContentType initialContentType)
1052    {
1053        List<MetadataDefinition> metadataDefinitionsByPath = getMetadataDefinitionPath(metadataPath, initialContentType);
1054        return metadataDefinitionsByPath.size() > 0 ? metadataDefinitionsByPath.get(metadataDefinitionsByPath.size() - 1) : null;
1055    }
1056    
1057    /**
1058     * Get a flat map of MetadataDefinition corresponding to a set of metadata paths
1059     * @param metadataPaths A set of metadata path to analyse
1060     * @param initialContentType The initial content type to start the search
1061     * @return The flat map of metadatapath and MetadataDefinition
1062     */
1063    public Map<String, MetadataDefinition> getMetadataDefinitions(Set<String> metadataPaths, ContentType initialContentType)
1064    {
1065        Map<String, MetadataDefinition> results = new HashMap<>();
1066        
1067        for (String metadataPath : metadataPaths)
1068        {
1069            results.put(metadataPath, getMetadataDefinition(metadataPath, initialContentType));
1070        }
1071        
1072        return results;
1073    }
1074
1075    /**
1076     * Get a flat map of MetadataDefinition corresponding to a metadataset
1077     * @param metadataSet A metadataset to analyse
1078     * @param initialContentType The initial content type to start the search
1079     * @return The flat map of metadatapath and MetadataDefinition
1080     */
1081    public Map<String, MetadataDefinition> getMetadataDefinitions(MetadataSet metadataSet, ContentType initialContentType)
1082    {
1083        return getMetadataDefinitions(getMetadataPaths(metadataSet), initialContentType);
1084    }
1085    
1086    /** 
1087     * Retrieve the list of successive metadata definitions represented by the given path.
1088     * The path can represent a metadata on another content.
1089     * @param initialContentType The initial content type to start the search
1090     * @param metadataPath the metadata path separated by '/'
1091     * @return the list of metadata definitions, one by path element.
1092     */ 
1093    public List<MetadataDefinition> getMetadataDefinitionPath(String metadataPath, ContentType initialContentType)
1094    {
1095        List<MetadataDefinition> definitions = new ArrayList<>();
1096        
1097        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1098        
1099        if (pathSegments.length > 0)
1100        {
1101            MetadataDefinition metadataDef = initialContentType.getMetadataDefinition(pathSegments[0]);
1102            
1103            if (metadataDef != null)
1104            {
1105                definitions.add(metadataDef);
1106            }
1107            else
1108            {
1109                return Collections.emptyList();
1110            }
1111            
1112            for (int i = 1; i < pathSegments.length; i++)
1113            {
1114                if (metadataDef.getType() == MetadataType.CONTENT || metadataDef.getType() == MetadataType.SUB_CONTENT)
1115                {
1116                    String refCTypeId = metadataDef.getContentType();
1117                    if (refCTypeId != null && _getContentTypeEP().hasExtension(refCTypeId))
1118                    {
1119                        ContentType refCType = _getContentTypeEP().getExtension(refCTypeId);
1120                        
1121                        List<MetadataDefinition> followingDefs = getMetadataDefinitionPath(StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, i, pathSegments.length), refCType);
1122                        if (CollectionUtils.isEmpty(followingDefs))
1123                        {
1124                            return Collections.emptyList();
1125                        }
1126                        definitions.addAll(followingDefs);
1127                        
1128                        return definitions;
1129                    }
1130                    else if ("title".equals(pathSegments[i]))
1131                    {
1132                        // No specific content type: allow only title.
1133                        definitions.add(getTitleMetadataDefinition());
1134                        return definitions;
1135                    }
1136                    else
1137                    {
1138                        return Collections.emptyList();
1139                    }
1140                }
1141                else
1142                {
1143                    metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
1144                    if (metadataDef != null)
1145                    {
1146                        definitions.add(metadataDef);
1147                    }
1148                    else
1149                    {
1150                        return Collections.emptyList();
1151                    }
1152                }
1153            }
1154        }
1155        
1156        return definitions;
1157    }
1158    
1159    /**
1160     * Get a flat map of MetadataDefinition path corresponding to a set of metadata paths
1161     * @param metadataPaths A set of metadata path to analyse
1162     * @param initialContentType The initial content type to start the search
1163     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
1164     */
1165    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(Set<String> metadataPaths, ContentType initialContentType)
1166    {
1167        Map<String, List<MetadataDefinition>> results = new HashMap<>();
1168        
1169        for (String metadataPath : metadataPaths)
1170        {
1171            results.put(metadataPath, getMetadataDefinitionPath(metadataPath, initialContentType));
1172        }
1173        
1174        return results;
1175    }
1176    
1177    /**
1178     * Get a flat map of MetadataDefinition path corresponding to a metadataSet
1179     * @param metadataSet A semetadataSet to analyse
1180     * @param initialContentType The initial content type to start the search
1181     * @return The flat map of metadatapath and corresponding list of MetadataDefinition path
1182     */
1183    public Map<String, List<MetadataDefinition>> getMetadataDefinitionsPaths(MetadataSet metadataSet, ContentType initialContentType)
1184    {
1185        return getMetadataDefinitionsPaths(getMetadataPaths(metadataSet), initialContentType);
1186    }
1187    
1188    /**
1189     * Determines if the given content type can be added to content
1190     * 
1191     * @param content The content
1192     * @param cTypeId The id of content type
1193     * @return <code>true</code> if the content type is compatible with content
1194     */
1195    public boolean isCompatibleContentType(Content content, String cTypeId)
1196    {
1197        String[] currentContentTypes = ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
1198
1199        ArrayList<String> cTypes = new ArrayList<>(Arrays.asList(currentContentTypes));
1200        cTypes.add(cTypeId);
1201
1202        try
1203        {
1204            getMetadataDefinitions(cTypes.toArray(new String[cTypes.size()]));
1205            return true;
1206        }
1207        catch (ConfigurationException e)
1208        {
1209            return false;
1210        }
1211    }
1212
1213    /**
1214     * Retrieves all definitions of a metadata resulting of the concatenation of
1215     * metadata of given content types.
1216     * 
1217     * @param cTypes The id of content types
1218     * @return the metadata definitions
1219     * @throws ConfigurationException if an error occurred
1220     */
1221    public Map<String, MetadataDefinition> getMetadataDefinitions(String[] cTypes) throws ConfigurationException
1222    {
1223        Map<String, MetadataDefinition> metadata = new LinkedHashMap<>();
1224
1225        for (String id : cTypes)
1226        {
1227            ContentType cType = _getContentTypeEP().getExtension(id);
1228
1229            for (String name : cType.getMetadataNames())
1230            {
1231                MetadataDefinition definition = cType.getMetadataDefinition(name);
1232
1233                if (metadata.containsKey(name))
1234                {
1235                    if (!definition.getReferenceContentType().equals(metadata.get(name).getReferenceContentType()))
1236                    {
1237                        // The definition does not provide from a common
1238                        // ancestor
1239                        throw new ConfigurationException("The metadata '" + name + "' defined in content-type '" + id + "' is already defined in another co-super-type '"
1240                                + metadata.get(name).getReferenceContentType() + "'");
1241                    }
1242                    continue;
1243                }
1244
1245                metadata.put(name, definition);
1246            }
1247        }
1248
1249        return metadata;
1250    }
1251    
1252    /**
1253     * Retrieves the common metadata definitions for a list of content types
1254     * @param cTypeIds The list of content types to consider
1255     * @param metadataSetName The metadata set name to list metadata
1256     * @param isEdition Is the metadata set for edition (or for view)
1257     * @return The map of metadata definition. Key are the metadata path in the content type
1258     */
1259    public Map<String, MetadataDefinition> getCommonMetadataDefinitions(Collection<String> cTypeIds, String metadataSetName, boolean isEdition)
1260    {
1261        Map<String, MetadataDefinition> commonMetadataDefinitions = null;
1262        
1263        for (String cTypeId : cTypeIds)
1264        {
1265            ContentType cType = _getContentTypeEP().getExtension(cTypeId);
1266            AbstractMetadataSetElement metadataSetElement = _getMetadataSet(cType, metadataSetName, isEdition);
1267            
1268            Map<String, MetadataDefinition> accumulator = new LinkedHashMap<>();
1269            if (metadataSetElement != null)
1270            {
1271                _getMetadataDefinitionsAcc(accumulator, metadataSetElement, "", cType);
1272            }
1273            
1274            if (commonMetadataDefinitions == null)
1275            {
1276                commonMetadataDefinitions = new LinkedHashMap<>();
1277                for (String name : accumulator.keySet())
1278                {
1279                    commonMetadataDefinitions.put(name, accumulator.get(name));
1280                }
1281            }
1282            else
1283            {
1284                // only retains common metadata (performs a set intersection)
1285                commonMetadataDefinitions.keySet().retainAll(accumulator.keySet());
1286            }
1287        }
1288        
1289        return commonMetadataDefinitions != null ? commonMetadataDefinitions : Collections.emptyMap();
1290    }
1291    
1292    /**
1293     * Populate the accumulator with the metadata definition
1294     * @param internalAcc the accumulator of metadata definition
1295     * @param metadataSetElement the metadata set of the content
1296     * @param prefix the path prefix preceding the names of the metadata
1297     * @param metaDefHolder the holder of the metadata definitions for the content
1298     */
1299    protected void _getMetadataDefinitionsAcc(Map<String, MetadataDefinition> internalAcc, AbstractMetadataSetElement metadataSetElement, String prefix, MetadataDefinitionHolder metaDefHolder)
1300    {
1301        if (metadataSetElement instanceof MetadataDefinitionReference)
1302        {
1303            MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) metadataSetElement;
1304            String subMetadataName = metadataDefRef.getMetadataName();
1305            MetadataDefinition subMetadataDef = metaDefHolder.getMetadataDefinition(subMetadataName);
1306
1307            if (subMetadataDef != null)
1308            {
1309                if (MetadataType.COMPOSITE.equals(subMetadataDef.getType()))
1310                {
1311                    for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
1312                    {
1313                        String path = prefix + subMetadataName;
1314                        internalAcc.put(path, subMetadataDef);
1315                        _getMetadataDefinitionsAcc(internalAcc, subElement, path + "/", subMetadataDef);
1316                    }
1317                }
1318                else
1319                {
1320                    String path = prefix + subMetadataName;
1321                    internalAcc.put(path, subMetadataDef);
1322                }
1323            }
1324        }
1325        else
1326        {
1327            for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
1328            {
1329                _getMetadataDefinitionsAcc(internalAcc, subElement, prefix, metaDefHolder);
1330            }
1331        }
1332    }
1333    
1334    /**
1335     * Get the metadataset of a content given the parameters
1336     * @param contentType The content type to consider
1337     * @param metadataSetName The metadata set to get (that list the metadata to consider)
1338     * @param isEdition Is the metadata set for edition (or for view)
1339     * @return The metadataset
1340     */
1341    protected MetadataSet _getMetadataSet(ContentType contentType, String metadataSetName, boolean isEdition)
1342    {
1343        String name = metadataSetName;
1344        if (StringUtils.isBlank(name))
1345        {
1346            name = "main";
1347        }
1348        
1349        MetadataSet metadataSet = null;
1350        
1351        if (isEdition)
1352        {
1353            metadataSet = contentType.getMetadataSetForEdition(name);
1354        }
1355        else
1356        {
1357            metadataSet = contentType.getMetadataSetForView(name);
1358        }
1359        
1360        return metadataSet;
1361    }
1362
1363    /**
1364     * Get information on content types.<br>
1365     * @return A Map with content types
1366     */
1367    public Set<Map<String, Object>> getContentTypesInformations()
1368    {
1369        Set<Map<String, Object>> result = new HashSet<>();
1370        
1371        // Collect content types.
1372        Collection<String> allContentTypesIds = _getContentTypeEP().getExtensionsIds();
1373        for (String id : allContentTypesIds)
1374        {
1375            ContentType cType = _getContentTypeEP().getExtension(id);
1376
1377            if (cType != null)
1378            {
1379                Map<String, Object> contentTypeProperties = getContentTypeProperties(cType);
1380                contentTypeProperties.put("rightEvaluated", _hasRight(cType));
1381                result.add(contentTypeProperties);
1382            }
1383        }
1384        
1385        return result;
1386    }
1387    
1388    /**
1389     * Get information on content types.<br>
1390     * 
1391     * @param ids The id of content types to retrieve
1392     * @param inherited If true, the sub-types will be also returned.
1393     * @param checkRights If true, only content types allowed for creation will
1394     *            be returned
1395     * @param includePrivate If true, the list will include the private content types. By default the list is restricted to the public content types.
1396     * @param includeMixins If true the list will include the mixins content types.
1397     * @param includeAbstract If true the list will include the abstract content types.
1398     * @return A Map with retrieved, unknown, not-allowed and private content types
1399     */
1400    @Callable
1401    public Map<String, Object> getContentTypesList (List<String> ids, boolean inherited, boolean checkRights, boolean includePrivate, boolean includeMixins, boolean includeAbstract)
1402    {
1403        // Collect content types.
1404        Collection<String> allContentTypesIds = new ArrayList<>();
1405        if (ids == null || ids.isEmpty())
1406        {
1407            allContentTypesIds = _getContentTypeEP().getExtensionsIds();
1408        }
1409        else
1410        {
1411            for (String id : ids)
1412            {
1413                _addIfNotPresent(allContentTypesIds, id);
1414    
1415                if (inherited)
1416                {
1417                    for (String subTypeId : _getContentTypeEP().getSubTypes(id))
1418                    {
1419                        _addIfNotPresent(allContentTypesIds, subTypeId);
1420                    }
1421                }
1422            }
1423        }
1424
1425        // Resolve and organize content types
1426        return _dispatchContentTypes(checkRights, includePrivate, includeMixins, includeAbstract, allContentTypesIds);
1427    }
1428
1429    private Map<String, Object> _dispatchContentTypes(boolean checkRights, boolean includePrivate, boolean includeMixins, boolean includeAbstract, Collection<String> allContentTypesIds)
1430    {
1431        List<Map<String, Object>> contentTypes = new ArrayList<>();
1432        List<String> unknownContentTypes = new ArrayList<>();
1433        List<String> noRightContentTypes = new ArrayList<>();
1434        List<String> privateContentTypes = new ArrayList<>();
1435        List<String> mixinContentTypes = new ArrayList<>();
1436        List<String> abstractContentTypes = new ArrayList<>();
1437
1438        for (String id : allContentTypesIds)
1439        {
1440            ContentType cType = _getContentTypeEP().getExtension(id);
1441
1442            if (cType != null)
1443            {
1444                if (cType.isAbstract() && !includeAbstract)
1445                {
1446                    abstractContentTypes.add(id);
1447                }
1448                else if (cType.isPrivate() && !includePrivate)
1449                {
1450                    privateContentTypes.add(id);
1451                }
1452                else if (cType.isMixin() && !includeMixins)
1453                {
1454                    mixinContentTypes.add(id);
1455                }
1456                else if (!checkRights || _hasRight(cType))
1457                {
1458                    contentTypes.add(getContentTypeProperties(cType));
1459                }
1460                else
1461                {
1462                    noRightContentTypes.add(id);
1463                }
1464            }
1465            else
1466            {
1467                unknownContentTypes.add(id);
1468            }
1469        }
1470        
1471        Map<String, Object> result = new HashMap<>();
1472
1473        result.put("contentTypes", contentTypes);
1474        result.put("noRightContentTypes", noRightContentTypes);
1475        result.put("unknownContentTypes", unknownContentTypes);
1476        result.put("privateContentTypes", privateContentTypes);
1477        result.put("mixinContentTypes", mixinContentTypes);
1478        result.put("abstractContentTypes", abstractContentTypes);
1479
1480        return result;
1481    }
1482
1483    /**
1484     * Get the content type properties
1485     * 
1486     * @param contentType The content type
1487     * @return The content type properties
1488     */
1489    public Map<String, Object> getContentTypeProperties(ContentType contentType)
1490    {
1491        Map<String, Object> infos = new HashMap<>();
1492
1493        infos.put("id", contentType.getId());
1494        infos.put("label", contentType.getLabel());
1495        infos.put("description", contentType.getDescription());
1496        infos.put("defaultTitle", contentType.getDefaultTitle());
1497        infos.put("iconGlyph", contentType.getIconGlyph());
1498        infos.put("iconDecorator", contentType.getIconDecorator());
1499        infos.put("iconSmall", contentType.getSmallIcon());
1500        infos.put("iconMedium", contentType.getMediumIcon());
1501        infos.put("iconLarge", contentType.getLargeIcon());
1502        infos.put("right", contentType.getRight());
1503        infos.put("isMultilingual", contentType.isMultilingual());
1504        infos.put("isSimple", contentType.isSimple());
1505        infos.put("isPrivate", contentType.isPrivate());
1506        infos.put("isAbstract", contentType.isAbstract());
1507        infos.put("isReferenceTable", contentType.isReferenceTable());
1508        infos.put("isMixin", contentType.isMixin());
1509        infos.put("superTypes", contentType.getSupertypeIds());
1510        infos.put("tags", contentType.getTags());
1511        infos.put("parentMetadataName", Optional.ofNullable(contentType.getParentMetadata()).map(MetadataDefinition::getId).orElse(""));
1512        
1513        return infos;
1514    }
1515
1516    /**
1517     * Test if the current user has the right needed by the content type to create a content.
1518     * @param contentType The content type
1519     * @return true if the user has the right needed, false otherwise.
1520     */
1521    protected boolean _hasRight(ContentType contentType)
1522    {
1523        boolean hasRight = false;
1524
1525        String right = contentType.getRight();
1526
1527        if (right == null)
1528        {
1529            hasRight = true;
1530        }
1531        else
1532        {
1533            UserIdentity user = _userProvider.getUser();
1534            hasRight = _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
1535        }
1536
1537        return hasRight;
1538    }
1539
1540    private void _addIfNotPresent(Collection<String> collection, String value)
1541    {
1542        if (!collection.contains(value))
1543        {
1544            collection.add(value);
1545        }
1546    }
1547
1548    /**
1549     * Determine whether a metadata can be read at this time.
1550     * 
1551     * @param metadataDef the metadata definition
1552     * @param content The content where metadata is to be read on.
1553     * @return <code>true</code> if the current user is allowed to read the
1554     *         metadata of this content.
1555     * @throws AmetysRepositoryException if an error occurs while accessing the
1556     *             content.
1557     */
1558    public boolean canRead(Content content, MetadataDefinition metadataDef)
1559    {
1560        String id = metadataDef.getReferenceContentType();
1561        ContentType cType = _getContentTypeEP().getExtension(id);
1562        return cType.canRead(content, metadataDef);
1563    }
1564
1565    /**
1566     * Determine whether a metadata can be read at this time.
1567     * 
1568     * @param metadataDef the metadata definition
1569     * @param content The content where metadata is to be read on.
1570     * @return <code>true</code> if the current user is allowed to read the
1571     *         metadata of this content.
1572     * @throws AmetysRepositoryException if an error occurs while accessing the
1573     *             content.
1574     */
1575    public boolean canWrite(Content content, MetadataDefinition metadataDef)
1576    {
1577        String id = metadataDef.getReferenceContentType();
1578        ContentType cType = _getContentTypeEP().getExtension(id);
1579        return cType.canWrite(content, metadataDef);
1580    }
1581
1582    /**
1583     * Get the id of content type to use for rendering
1584     * 
1585     * @param content The content
1586     * @return the id of dynamic or standard content type
1587     */
1588    public String getContentTypeIdForRendering(Content content)
1589    {
1590        DynamicContentTypeDescriptor dynamicContentType = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1591        if (dynamicContentType != null)
1592        {
1593            return dynamicContentType.getId();
1594        }
1595        
1596        ContentType firstContentType = getFirstContentType(content);
1597        return firstContentType != null ? firstContentType.getId() : StringUtils.EMPTY;
1598    }
1599
1600    /**
1601     * Get the plugin name of content type to use for rendering
1602     * 
1603     * @param content The content
1604     * @return the plugin name of dynamic or standard content type
1605     */
1606    public String getContentTypePluginForRendering(Content content)
1607    {
1608        DynamicContentTypeDescriptor dynamicContentType = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1609        if (dynamicContentType != null)
1610        {
1611            return dynamicContentType.getPluginName();
1612        }
1613        
1614        ContentType firstContentType = getFirstContentType(content);
1615        return firstContentType != null ? firstContentType.getPluginName() : StringUtils.EMPTY;
1616    }
1617
1618    /**
1619     * Retrieves the label of the content type.
1620     * 
1621     * @param content The content
1622     * @return the label.
1623     */
1624    public I18nizableText getContentTypeLabel(Content content)
1625    {
1626        DynamicContentTypeDescriptor dynamicContentType = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1627        if (dynamicContentType != null)
1628        {
1629            return dynamicContentType.getLabel();
1630        }
1631        
1632        ContentType firstContentType = getFirstContentType(content);
1633        return firstContentType != null ? firstContentType.getLabel() : new I18nizableText(StringUtils.EMPTY);
1634    }
1635
1636    /**
1637     * Retrieves the description of the content type.
1638     * 
1639     * @param content The content
1640     * @return the label.
1641     */
1642    public I18nizableText getContentTypeDescription(Content content)
1643    {
1644        DynamicContentTypeDescriptor dynamicContentType = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1645        if (dynamicContentType != null)
1646        {
1647            return dynamicContentType.getDescription();
1648        }
1649        
1650        ContentType firstContentType = getFirstContentType(content);
1651        return firstContentType != null ? firstContentType.getDescription() : new I18nizableText(StringUtils.EMPTY);
1652    }
1653
1654    /**
1655     * Retrieves the default title of the content type.
1656     * 
1657     * @param content The content
1658     * @return the label.
1659     */
1660    public I18nizableText getContentTypeDefaultTitle(Content content)
1661    {
1662        DynamicContentTypeDescriptor dynamicContentType = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1663        if (dynamicContentType != null)
1664        {
1665            return dynamicContentType.getDefaultTitle();
1666        }
1667
1668        ContentType firstContentType = getFirstContentType(content);
1669        return firstContentType != null ? firstContentType.getDefaultTitle() : new I18nizableText(StringUtils.EMPTY);
1670    }
1671
1672    /**
1673     * Retrieves the category of the content type.
1674     * 
1675     * @param content The content
1676     * @return the label.
1677     */
1678    public I18nizableText getContentTypeCategory(Content content)
1679    {
1680        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1681        if (dynamicCTDescriptor != null)
1682        {
1683            return dynamicCTDescriptor.getCategory();
1684        }
1685        
1686        ContentType firstContentType = getFirstContentType(content);
1687        return firstContentType != null ? firstContentType.getCategory() : new I18nizableText(StringUtils.EMPTY);
1688    }
1689    
1690    /**
1691     * Retrieves the CSS class to use as glyph icon of the content
1692     * @param content The content
1693     * @return the glyph
1694     */
1695    public String getIconGlyph(Content content)
1696    {
1697        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1698        if (dynamicCTDescriptor != null)
1699        {
1700            return dynamicCTDescriptor.getIconGlyph();
1701        }
1702        
1703        ContentType firstContentType = getFirstContentType(content);
1704        return firstContentType != null ? firstContentType.getIconGlyph() : null;
1705    }
1706    
1707    /**
1708     * Retrieves the CSS class to use as decorator above the main icon
1709     * @param content The content
1710     * @return the decorator CSS class name
1711     */
1712    public String getIconDecorator(Content content)
1713    {
1714        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1715        if (dynamicCTDescriptor != null)
1716        {
1717            return dynamicCTDescriptor.getIconDecorator();
1718        }
1719        
1720        ContentType firstContentType = getFirstContentType(content);
1721        return firstContentType != null ? firstContentType.getIconDecorator() : null;
1722    }
1723
1724    /**
1725     * Retrieves the URL of the icon without the context path.
1726     * 
1727     * @param content The content
1728     * @return the icon URL for the small image 16x16.
1729     */
1730    public String getSmallIcon(Content content)
1731    {
1732        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1733        if (dynamicCTDescriptor != null)
1734        {
1735            return dynamicCTDescriptor.getSmallIcon();
1736        }
1737        
1738        ContentType firstContentType = getFirstContentType(content);
1739        return firstContentType != null ? firstContentType.getSmallIcon() : StringUtils.EMPTY;
1740    }
1741
1742    /**
1743     * Retrieves the URL of the icon without the context path.
1744     * 
1745     * @param content The content
1746     * @return the icon URL for the medium image 32x32.
1747     */
1748    public String getMediumIcon(Content content)
1749    {
1750        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1751        if (dynamicCTDescriptor != null)
1752        {
1753            return dynamicCTDescriptor.getMediumIcon();
1754        }
1755        
1756        ContentType firstContentType = getFirstContentType(content);
1757        return firstContentType != null ? firstContentType.getMediumIcon() : StringUtils.EMPTY;
1758    }
1759
1760    /**
1761     * Retrieves the URL of the icon without the context path.
1762     * 
1763     * @param content The content
1764     * @return the icon URL for the large image 48x48.
1765     */
1766    public String getLargeIcon(Content content)
1767    {
1768        DynamicContentTypeDescriptor dynamicCTDescriptor = _dynamicCTDescriptorEP.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1769        if (dynamicCTDescriptor != null)
1770        {
1771            return dynamicCTDescriptor.getLargeIcon();
1772        }
1773        
1774        ContentType firstContentType = getFirstContentType(content);
1775        return firstContentType != null ? firstContentType.getLargeIcon() : StringUtils.EMPTY;
1776    }
1777
1778    /**
1779     * Get the content type which determines the content icons and rendering
1780     * 
1781     * @param content The content
1782     * @return The main content type
1783     */
1784    public ContentType getFirstContentType(Content content)
1785    {
1786        TreeSet<ContentType> treeSet = new TreeSet<>(new ContentTypeComparator());
1787
1788        for (String id : content.getTypes())
1789        {
1790            ContentType contentType = _getContentTypeEP().getExtension(id);
1791            if (contentType != null)
1792            {
1793                treeSet.add(contentType);
1794            }
1795            else
1796            {
1797                if (getLogger().isWarnEnabled())
1798                {
1799                    getLogger().warn(String.format("Trying to get an unknown content type : '%s'.", id));
1800                }
1801            }
1802        }
1803        
1804        return !treeSet.isEmpty() ? treeSet.first() : null;
1805    }
1806    
1807    /**
1808     * Get the metadata definition for the "title" standard metadata.
1809     * @return The standard title metadata definition.
1810     */
1811    public static MetadataDefinition getTitleMetadataDefinition()
1812    {
1813        MetadataDefinition def = new MetadataDefinition();
1814        def.setId("title");
1815        def.setName("title");
1816        def.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_METADATA_TITLE_LABEL"));
1817        def.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_METADATA_TITLE_DESCRIPTION"));
1818        def.setType(MetadataType.STRING);
1819        def.setMultiple(false);
1820        
1821        return def;
1822    }
1823
1824    class ContentTypeComparator implements Comparator<ContentType>
1825    {
1826        @Override
1827        public int compare(ContentType c1, ContentType c2)
1828        {
1829            I18nizableText t1 = c1.getLabel();
1830            I18nizableText t2 = c2.getLabel();
1831
1832            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
1833            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
1834
1835            int compareTo = str1.toString().compareTo(str2.toString());
1836            if (compareTo == 0)
1837            {
1838                // Content types have same keys but there are not equals, so do
1839                // not return 0 to add it in TreeSet
1840                // Indeed, in a TreeSet implementation two elements that are
1841                // equal by the method compareTo are, from the standpoint of the
1842                // set, equal
1843                return 1;
1844            }
1845            return compareTo;
1846        }
1847    }
1848
1849    private String _getCacheIdentifier(String[] contentTypes, String[] mixins)
1850    {
1851        Arrays.sort(contentTypes);
1852        String name = StringUtils.join(contentTypes, ";");
1853
1854        if (mixins.length > 0)
1855        {
1856            Arrays.sort(mixins);
1857            name += ";";
1858            name += StringUtils.join(mixins, ";");
1859        }
1860
1861        return name;
1862    }
1863}