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