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.Function;
034import java.util.stream.Collectors;
035
036import org.apache.avalon.framework.activity.Disposable;
037import org.apache.avalon.framework.activity.Initializable;
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.logger.AbstractLogEnabled;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.avalon.framework.thread.ThreadSafe;
044import org.apache.cocoon.ProcessingException;
045import org.apache.commons.lang3.ArrayUtils;
046import org.apache.commons.lang3.StringUtils;
047import org.apache.commons.lang3.tuple.Pair;
048
049import org.ametys.cms.content.RootContentHelper;
050import org.ametys.cms.data.type.ModelItemTypeExtensionPoint;
051import org.ametys.cms.repository.Content;
052import org.ametys.core.cache.AbstractCacheManager;
053import org.ametys.core.cache.Cache;
054import org.ametys.core.right.RightManager;
055import org.ametys.core.right.RightManager.RightResult;
056import org.ametys.core.ui.Callable;
057import org.ametys.core.user.CurrentUserProvider;
058import org.ametys.core.user.UserIdentity;
059import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
060import org.ametys.runtime.i18n.I18nizableText;
061import org.ametys.runtime.model.DefinitionContext;
062import org.ametys.runtime.model.ElementDefinition;
063import org.ametys.runtime.model.ModelHelper;
064import org.ametys.runtime.model.ModelHelper.ConfigurationAndPluginName;
065import org.ametys.runtime.model.ModelItem;
066import org.ametys.runtime.model.ModelViewItem;
067import org.ametys.runtime.model.View;
068import org.ametys.runtime.model.ViewElement;
069import org.ametys.runtime.model.ViewItem;
070import org.ametys.runtime.model.ViewItemContainer;
071import org.ametys.runtime.model.exception.UndefinedItemPathException;
072import org.ametys.runtime.model.type.ModelItemTypeConstants;
073
074/**
075 * Helper for manipulating {@link ContentType}s
076 */
077public class ContentTypesHelper extends AbstractLogEnabled implements Component, Serviceable, ThreadSafe, Disposable, Initializable
078{
079    /** The Avalon role */
080    public static final String ROLE = ContentTypesHelper.class.getName();
081
082    /** Archived content type */
083    public static final String ARCHIVED_CONTENT_TYPE = "org.ametys.cms.ArchivedContent";
084
085    private static final String __VIEW_METADATASET = ContentTypesHelper.class.getName() + "$view.metadataset";
086    
087    private static final String __EDITION_METADATASET = ContentTypesHelper.class.getName() + "$edition.metadataset";
088    
089    private static final String __VIEW_CACHE = ContentTypesHelper.class.getName() + "$view.cache";
090
091    /** The content types extension point */
092    protected ContentTypeExtensionPoint _cTypeEP;
093    /** The current user provider */
094    protected CurrentUserProvider _userProvider;
095    /** The rights manager */
096    protected RightManager _rightManager;
097    /** Helper for root content */
098    protected RootContentHelper _rootContentHelper;
099    /** The extension point with the available types for contents */
100    protected ModelItemTypeExtensionPoint _contentAttributeTypeExtensionPoint;
101    
102    private DynamicContentTypeDescriptorExtentionPoint _dynamicCTDescriptorEP;
103
104    private ServiceManager _smanager;
105
106    private AbstractCacheManager _cacheManager;
107    
108    @Override
109    public void service(ServiceManager smanager) throws ServiceException
110    {
111        _smanager = smanager;
112        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
113        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
114        _rootContentHelper = (RootContentHelper) smanager.lookup(RootContentHelper.ROLE);
115        _contentAttributeTypeExtensionPoint = (ModelItemTypeExtensionPoint) smanager.lookup(ModelItemTypeExtensionPoint.ROLE_CONTENT_ATTRIBUTE);
116        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
117    }
118
119    /**
120     * Initialize
121     */
122    @Override
123    public void initialize()
124    {
125        _createCaches();
126    }
127    
128    /**
129     * Creates the caches
130     */
131    protected void _createCaches()
132    {
133        _cacheManager.createMemoryCache(__VIEW_METADATASET, 
134                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_VIEW_METADATASET_LABEL"),
135                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_VIEW_METADATASET_DESCRIPTION"),
136                true,
137                null);
138        _cacheManager.createMemoryCache(__EDITION_METADATASET, 
139                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_EDITION_METADATASET_LABEL"),
140                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_EDITION_METADATASET_DESCRIPTION"),
141                true,
142                null);
143        _cacheManager.createMemoryCache(__VIEW_CACHE, 
144                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_VIEW_LABEL"),
145                new I18nizableText("plugin.cms", "PLUGINS_CMS_CACHE_VIEW_DESCRIPTION"),
146                true,
147                null);
148    }
149    
150    private DynamicContentTypeDescriptorExtentionPoint _getDynamicContentTypeDescriptorExtentionPoint()
151    {
152        if (_dynamicCTDescriptorEP == null)
153        {
154            try
155            {
156                _dynamicCTDescriptorEP = (DynamicContentTypeDescriptorExtentionPoint) _smanager.lookup(DynamicContentTypeDescriptorExtentionPoint.ROLE);
157            }
158            catch (ServiceException e)
159            {
160                throw new IllegalStateException(e);
161            }
162        }
163        return _dynamicCTDescriptorEP;
164    }
165
166    @Override
167    public void dispose()
168    {
169        _getViewCache().invalidateAll();
170    }
171    
172    /**
173     * Lazy lookup of {@link ContentTypeExtensionPoint}
174     * @return the content type extension point
175     */
176    protected ContentTypeExtensionPoint _getContentTypeEP()
177    {
178        if (_cTypeEP == null)
179        {
180            try
181            {
182                _cTypeEP = (ContentTypeExtensionPoint) _smanager.lookup(ContentTypeExtensionPoint.ROLE);
183            }
184            catch (ServiceException e)
185            {
186                throw new RuntimeException("Unable to lookup ContentTypeExtensionPoint component", e);
187            }
188        }
189        return _cTypeEP;
190    }
191
192    /**
193     * Determines if a content is a instance of given content type id
194     * 
195     * @param content The content
196     * @param cTypeId The id of content type or mixin
197     * @return <code>true</code> if the content is an instance of content type
198     */
199    public boolean isInstanceOf(Content content, String cTypeId)
200    {
201        String[] types = content.getTypes();
202        if (ArrayUtils.contains(types, cTypeId))
203        {
204            return true;
205        }
206
207        String[] mixins = content.getMixinTypes();
208        if (ArrayUtils.contains(mixins, cTypeId))
209        {
210            return true;
211        }
212
213        return _containsContentType(ArrayUtils.addAll(types, mixins), cTypeId);
214    }
215
216    private boolean _containsContentType(String[] cTypesId, String cTypeId)
217    {
218        for (String id : cTypesId)
219        {
220            ContentType cType = _getContentTypeEP().getExtension(id);
221            if (cType != null)
222            {
223                if (ArrayUtils.contains(cType.getSupertypeIds(), cTypeId))
224                {
225                    return true;
226                }
227                else if (_containsContentType(cType.getSupertypeIds(), cTypeId))
228                {
229                    return true;
230                }
231            }
232        }
233        return false;
234    }
235    
236    /**
237     * Get the identifiers of the content types common ancestors
238     * @param contentTypeIds The identifiers of the content types to compare
239     * @return The identifiers of common ancestors
240     */
241    public Set<String> getCommonAncestors(Collection<String> contentTypeIds)
242    {
243        Set<String> commonAncestors = new HashSet<>();
244        
245        // Get ancestors of each content type
246        List<Collection<String>> superTypeIdsByCType = new ArrayList<>();
247        for (String contentTypeId : contentTypeIds)
248        {
249            Set<String> superTypeIds = new HashSet<>();
250
251            superTypeIds.add(contentTypeId);
252            superTypeIds.addAll(getAncestors(contentTypeId));
253
254            superTypeIdsByCType.add(superTypeIds);
255        }
256        
257        // Make the intersection of all the ancestors collections
258        if (!superTypeIdsByCType.isEmpty())
259        {
260            Iterator<Collection<String>> superTypeIdsByCTypeIt = superTypeIdsByCType.iterator();
261            commonAncestors.addAll(superTypeIdsByCTypeIt.next());
262            while (superTypeIdsByCTypeIt.hasNext() && !commonAncestors.isEmpty())
263            {
264                commonAncestors.retainAll(superTypeIdsByCTypeIt.next());
265            }
266        }
267        
268        // Remove ancestors of ancestors: their attributes will be given by the first one
269        commonAncestors = removeAncestors(commonAncestors);
270        
271        return commonAncestors;
272    }
273
274    /**
275     * Remove all content types in the set that are ancestors of other content types in the set.
276     * @param contentTypes a Set of content type IDs.
277     * @return a Set of content type IDs without ancestors.
278     */
279    protected Set<String> removeAncestors(Set<String> contentTypes)
280    {
281        Set<String> noAncestors = new HashSet<>(contentTypes);
282        
283        Iterator<String> it1 = contentTypes.iterator();
284        while (it1.hasNext())
285        {
286            ContentType cType1 = _getContentTypeEP().getExtension(it1.next());
287            
288            Iterator<String> it2 = contentTypes.iterator();
289            while (it2.hasNext())
290            {
291                String cType2 = it2.next();
292                String[] supertypeIds = cType1.getSupertypeIds();
293                
294                // CType2 is an ancestor of CType1: remove it.
295                if (ArrayUtils.contains(supertypeIds, cType2))
296                {
297                    noAncestors.remove(cType2);
298                }
299            }
300        }
301        
302        return noAncestors;
303    }
304
305    /**
306     * Get all ancestors for the given content type
307     * 
308     * @param contentTypeId The content type id to test
309     * @return A non-null set of all ancestors. Does not contains the contentTypeId itself.
310     * @throws IllegalArgumentException if the content type does not exist.
311     */
312    public Set<String> getAncestors(String contentTypeId)
313    {
314        Set<String> superTypes = new HashSet<>();
315
316        ContentType cType = _getContentTypeEP().getExtension(contentTypeId);
317        
318        if (cType == null)
319        {
320            throw new IllegalArgumentException("Unable to get anscestors of unknown content type '" + contentTypeId + "'");
321        }
322        
323
324        String[] supertypeIds = cType.getSupertypeIds();
325
326        for (String superTypeId : supertypeIds)
327        {
328            superTypes.add(superTypeId);
329            superTypes.addAll(getAncestors(superTypeId));
330        }
331
332        return superTypes;
333
334    }
335    
336    /**
337     * Get super type's ids for the given content type
338     * The first entry contains super content types, the second one contains super mixin types 
339     * 
340     * @param contentTypeId The content type id to test
341     * @return An array of super type's ids.
342     * @throws IllegalArgumentException if the content type does not exist.
343     */
344    public Pair<String[], String[]> getSupertypeIds(String contentTypeId)
345    {
346        ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
347        if (contentType == null)
348        {
349            throw new IllegalArgumentException("Unable to get super types of unknown content type '" + contentTypeId + "'");
350        }
351
352        List<String> superMixins = new ArrayList<>();
353        List<String> superContentTypes = new ArrayList<>();
354        for (String supertypeId : contentType.getSupertypeIds())
355        {
356            ContentType supertype = _getContentTypeEP().getExtension(supertypeId);
357            if (supertype == null)
358            {
359                throw new IllegalArgumentException("Unable to get the unknown super type '" + supertypeId + "' for type '" + contentTypeId + "'");
360            }
361
362            if (supertype.isMixin())
363            {
364                superMixins.add(supertypeId);
365            }
366            else
367            {
368                superContentTypes.add(supertypeId);
369            }
370        }
371        
372        String[] superContentTypesArray = superContentTypes.toArray(new String[superContentTypes.size()]);
373        String[] superMixinsArray = superMixins.toArray(new String[superMixins.size()]);
374        return Pair.of(superContentTypesArray, superMixinsArray);
375    }
376
377    /**
378     * Get plugin name for the given content type
379     * 
380     * @param contentTypeId The content type id to test
381     * @return the plugin name
382     * @throws IllegalArgumentException if the content type does not exist.
383     */
384    public String getPluginName(String contentTypeId)
385    {
386        ContentType cType = _getContentTypeEP().getExtension(contentTypeId);
387        if (cType == null)
388        {
389            throw new IllegalArgumentException("Unable to get plugin name of unknown content type '" + contentTypeId + "'");
390        }
391
392        return cType.getPluginName();
393    }
394    
395    /**
396     * Builds the reverse hierarchies of ancestors of a content type
397     * @param contentTypeId The content type's id
398     * @return the reverse hierarchies with ancestors
399     */
400    public List<Set<String>> buildReverseHierarchies(String contentTypeId)
401    {
402        Set<String> hierarchy = new LinkedHashSet<>();
403        hierarchy.add(contentTypeId);
404        return _buildReverseHierarchies (contentTypeId, hierarchy);
405    }
406    
407    private List<Set<String>> _buildReverseHierarchies(String contentTypeId, Set<String> hierarchy)
408    {
409        List<Set<String>> hierarchies = new ArrayList<>();
410        
411        ContentType cType = _getContentTypeEP().getExtension(contentTypeId);
412        if (cType == null)
413        {
414            throw new IllegalArgumentException("Unable to get anscestors of unknown content type '" + contentTypeId + "'");
415        }
416        
417        String[] supertypeIds = cType.getSupertypeIds();
418        if (supertypeIds.length > 0)
419        {
420            for (String superTypeId : supertypeIds)
421            {
422                Set<String> superHierarchy = new LinkedHashSet<>(hierarchy);
423                superHierarchy.add(superTypeId);
424                hierarchies.addAll(_buildReverseHierarchies(superTypeId, superHierarchy));
425            }
426        }
427        else
428        {
429            hierarchies.add(hierarchy);
430        }
431        
432        return hierarchies;
433    }
434    
435    /**
436     * Get all views resulting of the concatenation of views of given content types and mixins.
437     * @param contentTypeIds the identifiers of the content types
438     * @param mixinIds the identifiers of the mixins
439     * @return The views
440     */
441    public Map<String, View> getViews(String[] contentTypeIds, String[] mixinIds)
442    {
443        return getViews(contentTypeIds, mixinIds, Collections.emptySet());
444    }
445    
446    /**
447     * Get all views resulting of the concatenation of views of given content types and mixins.
448     * @param contentTypeIds the identifiers of the content types
449     * @param mixinIds the identifiers of the mixins
450     * @param viewNamesToAvoid names of views that should not be managed
451     * @return The views
452     */
453    public Map<String, View> getViews(String[] contentTypeIds, String[] mixinIds, Set<String> viewNamesToAvoid)
454    {
455        Map<String, View> views = new HashMap<>();
456        
457        for (String contentTypeId : contentTypeIds)
458        {
459            ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
460            for (String viewName : contentType.getViewNames())
461            {
462                if (!viewNamesToAvoid.contains(viewName) && !views.containsKey(viewName))
463                {
464                    views.put(viewName, getView(viewName, contentTypeIds, mixinIds));
465                }
466            }
467        }
468        
469        return views;
470    }
471    
472    /**
473     * Get the view for view resulting of the concatenation of views of the given content.
474     * @param viewName the name of the view to retrieve
475     * @param content the given content
476     * @return The view or null if none matches.
477     */
478    public View getView(String viewName, Content content)
479    {
480        return getView(viewName, content.getTypes(), content.getMixinTypes());
481    }
482    
483    /**
484     * Get the view for view resulting of the concatenation of views of the given content types and mixins.
485     * @param viewName the name of the view to retrieve
486     * @param contentTypeIds the identifiers of the content types
487     * @param mixinIds the identifiers of the mixins
488     * @return The view or null if none matches.
489     */
490    public View getView(String viewName, String[] contentTypeIds, String[] mixinIds)
491    {
492        CacheKey cacheKey = CacheKey.of(Set.of(contentTypeIds), Set.of(mixinIds), viewName);
493        return _getViewCache().get(cacheKey,  __ -> _computeView(viewName, contentTypeIds, mixinIds));
494    }
495    
496    /**
497     * Get the view for view resulting for a given content
498     * @param viewName the name of the view to retrieve. If null or empty, fallback view will be used.
499     * @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.
500     * @param content the content
501     * @return The view or null if none matches.
502     */
503    public View getViewWithFallback(String viewName, String fallbackViewName, Content content)
504    {
505        return getViewWithFallback(viewName, fallbackViewName, content.getTypes(), content.getMixinTypes());
506    }
507    
508    /**
509     * Get the view for view resulting of the concatenation of views of the given content types and mixins.
510     * @param viewName the name of the view to retrieve. If null or empty, fallback view will be used.
511     * @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.
512     * @param contentTypeIds the identifiers of the content types
513     * @param mixinIds the identifiers of the mixins
514     * @return The view or null if none matches.
515     */
516    public View getViewWithFallback(String viewName, String fallbackViewName, String[] contentTypeIds, String[] mixinIds)
517    {
518        String usedFallbackViewName = fallbackViewName;
519        if (StringUtils.isBlank(fallbackViewName))
520        {
521            usedFallbackViewName = "main";
522        }
523        
524        // Use the fallbackViewName if no viewName is provided
525        String usedViewName = viewName;
526        if (StringUtils.isBlank(usedViewName))
527        {
528            usedViewName = usedFallbackViewName;
529        }
530        
531        View view = getView(usedViewName, contentTypeIds, mixinIds);
532        
533        if (view == null && !usedViewName.equals(usedFallbackViewName))
534        {
535            view = getView(usedFallbackViewName, contentTypeIds, mixinIds);
536        }
537        
538        return view;
539    }
540
541    /**
542     * Converts the view with the given name in a JSON Map
543     * @param contentTypeId the content type identifier
544     * @param viewName the name of the view to convert
545     * @param isEdition <code>true</code> if the JSON result is used for edition purposes (configure a form panel, ...) <code>false</code> otherwise
546     * @return the view as a JSON Map
547     * @throws ProcessingException if an error occurs when converting the view
548     */
549    @Callable
550    public Map<String, Object> getViewAsJSON(String contentTypeId, String viewName, boolean isEdition) throws ProcessingException
551    {
552        Map<String, Object> json = new LinkedHashMap<>();
553        
554        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
555        json.put("contentType", Map.of(
556                "id", contentTypeId,
557                "label", contentType.getLabel(),
558                "defaultTitle", contentType.getDefaultTitle()));
559        
560        View view = contentType.getView(viewName);
561        if (view != null)
562        {
563            json.put("view", view.toJSON(DefinitionContext.newInstance().withEdition(isEdition)));
564        }
565        else
566        {
567            if (getLogger().isWarnEnabled())
568            {
569                getLogger().warn(String.format("Unknown view '%s' for content type '%s'", viewName, contentType.getId()));
570            }
571        }
572        
573        return json;
574    }
575    
576    /**
577     * Converts the title view in a JSON Map
578     * @param contentTypeId the content type identifier
579     * @param isEdition <code>true</code> if the JSON result is used for edition purposes (configure a form panel, ...) <code>false</code> otherwise
580     * @return the view as a JSON Map
581     * @throws ProcessingException if an error occurs when converting the view
582     */
583    @Callable
584    public Map<String, Object> getTitleViewAsJSON(String contentTypeId, boolean isEdition) throws ProcessingException
585    {
586        Map<String, Object> json = new LinkedHashMap<>();
587        
588        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
589        json.put("contentType", Map.of(
590                "id", contentTypeId,
591                "label", contentType.getLabel(),
592                "defaultTitle", contentType.getDefaultTitle()));
593        
594        View titleView = new View();
595        
596        ModelItem modelItem = contentType.getModelItem("title");
597        ViewElement viewElement = new ViewElement();
598        viewElement.setDefinition((ElementDefinition) modelItem);
599        titleView.addViewItem(viewElement);
600        
601        json.put("view", titleView.toJSON(DefinitionContext.newInstance().withEdition(isEdition)));
602        return json;
603    }
604    
605    private View _computeView(String viewName, String[] contentTypeIds, String[] mixinIds)
606    {
607        List<View> views = new ArrayList<>();
608        for (String contentTypeId : contentTypeIds)
609        {
610            ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
611
612            View view = contentType.getView(viewName);
613            if (view != null)
614            {
615                views.add(view);
616            }
617        }
618
619        for (String id : mixinIds)
620        {
621            ContentType mixin = _getContentTypeEP().getExtension(id);
622
623            View view = mixin.getView(viewName);
624            if (view != null)
625            {
626                views.add(view);
627            }
628        }
629
630        return joinViews(views);
631    }
632    
633    /**
634     * Creates a view that is a jointure between all the given views
635     * @param views the views to join
636     * @return the joined view
637     */
638    public View joinViews(List<View> views)
639    {
640        if (views.isEmpty())
641        {
642            return null;
643        }
644        
645        View joinView = new View();
646        for (View view : views)
647        {
648            joinView.includeView(view);
649
650            if (joinView.getName() == null)
651            {
652                joinView.setName(view.getName());
653                joinView.setLabel(view.getLabel());
654                joinView.setDescription(view.getDescription());
655                joinView.setInternal(view.isInternal());
656                joinView.setIconGlyph(view.getIconGlyph());
657                joinView.setIconDecorator(view.getIconDecorator());
658                joinView.setSmallIcon(view.getSmallIcon());
659                joinView.setMediumIcon(view.getMediumIcon());
660                joinView.setLargeIcon(view.getLargeIcon());
661            }
662        }
663        return joinView;
664    }
665    
666    /**
667     * Stores all the configurations of the current content type for a view
668     * @param viewName the name of the view
669     * @param mainConfiguration the main configuration of the view, if it is configures on the current type
670     * @param overrides the list of overrides configured on the current type
671     */
672    public record ViewConfigurations(String viewName, Optional<ConfigurationAndPluginName> mainConfiguration, List<ConfigurationAndPluginName> overrides) { /* empty */ }
673    
674    /**
675     * Stores all the configurations for a view, by the content types in which the configuration is declared
676     * @param viewName the name of the view
677     * @param mainConfigurations the main configurations of the view
678     * @param overrides the list of overrides of the view
679     */
680    public record ViewConfigurationsByType(String viewName, Map<ContentType, ConfigurationAndPluginName> mainConfigurations, Map<ContentType, List<ConfigurationAndPluginName>> overrides) { /* empty */ }
681    
682    /**
683     * Get all configurations of the views resulting of the concatenation of views of given content types and mixins.
684     * @param contentTypeIds the identifiers of the content types
685     * @param mixinIds the identifiers of the mixins
686     * @return The views' configurations, indexed by view names and content type containing the configuration
687     */
688    public Map<String, ViewConfigurationsByType> getViewConfigurations(String[] contentTypeIds, String[] mixinIds)
689    {
690        Map<String, ViewConfigurationsByType> viewConfigurations = new HashMap<>();
691        for (String viewName : _getConfiguredViewNames(contentTypeIds))
692        {
693            getViewConfigurationsByType(viewName, contentTypeIds, mixinIds)
694                .ifPresent(currentViewConfigurations -> viewConfigurations.put(viewName, currentViewConfigurations));
695        }
696        
697        return viewConfigurations;
698    }
699    
700    private Set<String> _getConfiguredViewNames(String[] contentTypeIds)
701    {
702        List<ContentType> contentTypes = Arrays.stream(contentTypeIds)
703                                               .map(_getContentTypeEP()::getExtension)
704                                               .collect(Collectors.toList());
705        
706        // Create a Set with configured views of the given content types
707        Set<String> viewNames = contentTypes.stream()
708                .map(ContentType::getViewConfigurations)
709                .map(Map::keySet)
710                .flatMap(Collection::stream)
711                .collect(Collectors.toSet());
712
713        // Add views configured on given content types' super types
714        contentTypes.stream()
715                    .map(ContentType::getSupertypeIds)
716                    .map(this::_getConfiguredViewNames)
717                    .forEach(viewNames::addAll);
718        
719        return viewNames;
720    }
721    
722    /**
723     * Get all configurations of the given view resulting of the concatenation of given content types and mixins.
724     * @param viewName the name of the view
725     * @param contentTypeIds the identifiers of the content types
726     * @param mixinIds the identifiers of the mixins
727     * @return The views' configurations, indexed by content type containing the configuration
728     */
729    public Optional<ViewConfigurationsByType> getViewConfigurationsByType(String viewName, String[] contentTypeIds, String[] mixinIds)
730    {
731        Optional<ViewConfigurationsByType> viewConfigurationsByType = Optional.empty();
732        for (String contentTypeId : contentTypeIds)
733        {
734            ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
735            
736            // If there is a main configuration for the view in this type, add the confs in the record but do not check the configurations of the super types
737            if (_hasMainConfiguration(contentType, viewName))
738            {
739                ViewConfigurations currentTypeViewConfigurations = contentType.getViewConfigurations(viewName).get();
740                // Main configuration
741                Map<ContentType, ConfigurationAndPluginName> mainConfigurations = viewConfigurationsByType.map(ViewConfigurationsByType::mainConfigurations)
742                        .orElse(new LinkedHashMap<>());
743                currentTypeViewConfigurations.mainConfiguration().ifPresent(config -> mainConfigurations.put(contentType, config));
744
745                // overrides
746                Map<ContentType, List<ConfigurationAndPluginName>> overrides = viewConfigurationsByType.map(ViewConfigurationsByType::overrides)
747                        .orElse(new LinkedHashMap<>());
748                if (!currentTypeViewConfigurations.overrides().isEmpty())
749                {
750                    overrides.put(contentType, currentTypeViewConfigurations.overrides());
751                }
752
753                viewConfigurationsByType = Optional.of(new ViewConfigurationsByType(viewName, mainConfigurations, overrides));
754            }
755            else
756            {
757                Optional<ViewConfigurationsByType> parentsOptionalViewConfigurations = getViewConfigurationsByType(viewName, contentType.getSupertypeIds(), new String[0]);
758                if (parentsOptionalViewConfigurations.isPresent() && !parentsOptionalViewConfigurations.get().mainConfigurations().isEmpty())
759                {
760                    ViewConfigurationsByType parentsViewConfigurations = parentsOptionalViewConfigurations.get();
761
762                    // Merge parents main configurations to the current one
763                    Map<ContentType, ConfigurationAndPluginName> mainConfigurations = parentsViewConfigurations.mainConfigurations();
764                    viewConfigurationsByType.map(ViewConfigurationsByType::mainConfigurations)
765                                            .ifPresent(mainConfigurations::putAll);
766
767                    // Merge parents overrides to the current one. Put first the parent's overrides, the the child's one, and at the end the overrides that are already present
768                    Map<ContentType, List<ConfigurationAndPluginName>> overrides = parentsViewConfigurations.overrides();
769                    if (_hasOverrides(contentType, viewName))
770                    {
771                        ViewConfigurations currentTypeViewConfigurations = contentType.getViewConfigurations(viewName).get();
772                        overrides.put(contentType, currentTypeViewConfigurations.overrides());
773                    }
774                    viewConfigurationsByType.map(ViewConfigurationsByType::overrides)
775                                            .ifPresent(overrides::putAll);
776
777                    viewConfigurationsByType = Optional.of(new ViewConfigurationsByType(viewName, mainConfigurations, overrides));
778                }
779            }
780        }
781        
782        return viewConfigurationsByType;
783    }
784    
785    private boolean _hasMainConfiguration(ContentType contentType, String viewName)
786    {
787        Optional<ViewConfigurations> optionalViewConfigurations = contentType.getViewConfigurations(viewName);
788        return optionalViewConfigurations.isPresent() && optionalViewConfigurations.get().mainConfiguration().isPresent();
789    }
790    
791    private boolean _hasOverrides(ContentType contentType, String viewName)
792    {
793        Optional<ViewConfigurations> optionalViewConfigurations = contentType.getViewConfigurations(viewName);
794        return optionalViewConfigurations.isPresent() && !optionalViewConfigurations.get().overrides().isEmpty();
795    }
796    
797    /**
798     * Get the views of a content type
799     * @param contentTypeId the content type id
800     * @param includeInternals Set to true to include internal views.
801     * @return the views' info
802     */
803    public List<Map<String, Object>> getViewsInfo(String contentTypeId, boolean includeInternals)
804    {
805        List<Map<String, Object>> views = new ArrayList<>();
806        
807        ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
808        
809        Set<String> viewNames = contentType.getViewNames(includeInternals);
810        for (String viewName : viewNames)
811        {
812            View view = contentType.getView(viewName);
813            
814            Map<String, Object> viewInfos = new HashMap<>();
815            viewInfos.put("name", viewName);
816            viewInfos.put("label", view.getLabel());
817            viewInfos.put("description", view.getDescription());
818            views.add(viewInfos);
819        }
820        
821        return views;
822    }
823    
824    /**
825     * Retrieves the model item at the given path
826     * @param path path of the model item to retrieve. No matter if it is a definition or data path (with repeater entry positions)
827     * @param cTypes identifiers of the content types where to search the model item
828     * @param mixins identifiers of the mixins where to search the model item
829     * @return the model item
830     * @throws IllegalArgumentException if the given path is null or empty
831     * @throws UndefinedItemPathException if there is no item defined at the given path in given item containers
832     */
833    public ModelItem getModelItem(String path, String[] cTypes, String[] mixins) throws IllegalArgumentException, UndefinedItemPathException
834    {
835        Collection<ContentType> contentTypes = new ArrayList<>();
836
837        String[] allContentTypes = ArrayUtils.addAll(cTypes, mixins);
838        for (String contentTypeId : allContentTypes)
839        {
840            if (_getContentTypeEP().hasExtension(contentTypeId))
841            {
842                contentTypes.add(_getContentTypeEP().getExtension(contentTypeId));
843            }
844            else
845            {
846                if (getLogger().isWarnEnabled())
847                {
848                    getLogger().warn("Unknown content type identifier : " + contentTypeId);
849                }
850            }
851        }
852        
853        return ModelHelper.getModelItem(path, contentTypes);
854    }
855
856    /**
857     * Retrieve the list of successive model items represented by the given paths, indexed by path.
858     * @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)
859     * @param content the content
860     * @return the list of successive model items, indexed by path
861     * @throws IllegalArgumentException if one of the given paths is null or empty
862     * @throws UndefinedItemPathException if there is no item defined at one of the given paths in given item containers
863     */
864    public Map<String, List<ModelItem>> getModelItemsPaths(Set<String> paths, Content content)
865    {
866        return ModelHelper.getAllModelItemsInPaths(paths, content.getModel());
867    }
868    
869    /**
870     * Retrieve the list of successive model items represented by the given path.
871     * @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)
872     * @param content the content
873     * @return the list of successive model items
874     * @throws IllegalArgumentException if the given path is null or empty
875     * @throws UndefinedItemPathException if there is no item defined at the given path in given item containers
876     */
877    public List<ModelItem> getModelItemPath(String path, Content content) throws IllegalArgumentException, UndefinedItemPathException
878    {
879        return ModelHelper.getAllModelItemsInPath(path, content.getModel());
880    }
881    
882    /**
883     * Retrieve the list of successive model items represented by the given paths, indexed by path.
884     * @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)
885     * @param contentType the content type
886     * @return the list of successive model items, indexed by path
887     * @throws IllegalArgumentException if one of the given paths is null or empty
888     * @throws UndefinedItemPathException if there is no item defined at one of the given paths in given item containers
889     */
890    public Map<String, List<ModelItem>> getModelItemsPaths(Set<String> paths, ContentType contentType)
891    {
892        return ModelHelper.getAllModelItemsInPaths(paths, Collections.singleton(contentType));
893    }
894    
895    /**
896     * Retrieve the list of successive model items represented by the given path.
897     * @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)
898     * @param contentType the content type
899     * @return the list of successive model items
900     * @throws IllegalArgumentException if the given path is null or empty
901     * @throws UndefinedItemPathException if there is no item defined at the given path in given item containers
902     */
903    public List<ModelItem> getModelItemPath(String path, ContentType contentType)
904    {
905        return ModelHelper.getAllModelItemsInPath(path, Collections.singleton(contentType));
906    }
907    
908    /**
909     * Determines if the given content type can be added to content
910     * 
911     * @param content The content
912     * @param cTypeId The id of content type
913     * @return <code>true</code> if the content type is compatible with content
914     */
915    public boolean isCompatibleContentType(Content content, String cTypeId)
916    {
917        String[] currentContentTypes = ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
918
919        ArrayList<String> cTypes = new ArrayList<>(Arrays.asList(currentContentTypes));
920        cTypes.add(cTypeId);
921
922        try
923        {
924            getModelItems(cTypes.toArray(new String[cTypes.size()]));
925            return true;
926        }
927        catch (IllegalArgumentException e)
928        {
929            return false;
930        }
931    }
932
933    /**
934     * Retrieves all model items of given content types, indexed by their names.
935     * @param contentTypeIds The identifier of the content types
936     * @return the model items
937     * @throws IllegalArgumentException if some content types define a model item with the same name and this model item does not come from a common ancestor
938     */
939    public Map<String, ModelItem> getModelItemsIndexedByName(String[] contentTypeIds) throws IllegalArgumentException
940    {
941        return getModelItems(contentTypeIds).stream()
942                                            .collect(Collectors.toMap(ModelItem::getName, Function.identity()));
943    }
944    
945    /**
946     * Retrieves all model items of given content types.
947     * @param contentTypeIds The identifier of the content types
948     * @return the model items
949     * @throws IllegalArgumentException if some content types define a model item with the same name and this model item does not come from a common ancestor
950     */
951    public Collection<? extends ModelItem> getModelItems(String[] contentTypeIds) throws IllegalArgumentException
952    {
953        List<ContentType> contentTypes = Arrays.stream(contentTypeIds)
954                .map(id -> _getContentTypeEP().getExtension(id))
955                .collect(Collectors.toList());
956        
957        return ModelHelper.getModelItems(contentTypes);
958    }
959    
960    /**
961     * Retrieves the common model items for a list of content types
962     * @param contentTypeIds The list of content types to consider
963     * @param viewName The view name to list model items
964     * @return The map of model items. Key are the model item's path in the content type
965     */
966    public Map<String, ModelItem> getCommonModelItems(Collection<String> contentTypeIds, String viewName)
967    {
968        Map<String, ModelItem> commonModelItems = null;
969        
970        for (String contentTypeId : contentTypeIds)
971        {
972            ContentType contentType = _getContentTypeEP().getExtension(contentTypeId);
973            View view = contentType.getView(viewName);
974            
975            Map<String, ModelItem> accumulator = new LinkedHashMap<>();
976            if (view != null)
977            {
978                _populateModelItemsAccumulator(accumulator, view, StringUtils.EMPTY);
979            }
980            
981            if (commonModelItems == null)
982            {
983                commonModelItems = new LinkedHashMap<>();
984                for (Map.Entry<String, ModelItem> accumulatorEntry : accumulator.entrySet())
985                {
986                    commonModelItems.put(accumulatorEntry.getKey(), accumulatorEntry.getValue());
987                }
988            }
989            else
990            {
991                // only retains common attributes (performs a set intersection)
992                commonModelItems.keySet().retainAll(accumulator.keySet());
993            }
994        }
995        
996        return commonModelItems != null ? commonModelItems : Collections.emptyMap();
997    }
998    
999    /**
1000     * Populates the accumulator with the model items
1001     * @param internalAcc the accumulator of model items
1002     * @param viewItemContainer the view item container of the content
1003     * @param prefix the view item accessor path prefix
1004     */
1005    protected void _populateModelItemsAccumulator(Map<String, ModelItem> internalAcc, ViewItemContainer viewItemContainer, String prefix)
1006    {
1007        for (ViewItem viewItem : viewItemContainer.getViewItems())
1008        {
1009            String newPrefix = prefix;
1010            
1011            if (viewItem instanceof ModelViewItem)
1012            {
1013                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
1014                if (modelItem != null)
1015                {
1016                    internalAcc.put(modelItem.getPath(), modelItem);
1017
1018                    if (viewItem instanceof ViewItemContainer)
1019                    {
1020                        newPrefix += ModelItem.ITEM_PATH_SEPARATOR + modelItem.getName();
1021                    }
1022                }
1023            }
1024            
1025            if (viewItem instanceof ViewItemContainer)
1026            {
1027                _populateModelItemsAccumulator(internalAcc, (ViewItemContainer) viewItem, newPrefix);
1028            }
1029        }
1030    }
1031    
1032    /**
1033     * Get information on content types.<br>
1034     * @return A Map with content types
1035     */
1036    public Set<Map<String, Object>> getContentTypesInformations()
1037    {
1038        // Collect content types.
1039        List<String> allContentTypesIds = _getContentTypeEP()
1040                .getExtensionsIds()
1041                .stream()
1042                .collect(Collectors.toList());
1043        return getContentTypesInformations(allContentTypesIds, true);
1044    }
1045    
1046    /**
1047     * Get the content types information as JSON
1048     * @param contentTypeIds the list of content type ids
1049     * @param withRight true to add rights to results
1050     * @return the list of content types information as JSON
1051     */
1052    @Callable
1053    public Set<Map<String, Object>> getContentTypesInformations(List<String> contentTypeIds, boolean withRight)
1054    {
1055        Set<Map<String, Object>> results = new HashSet<>();
1056        for (String contentTypeId : contentTypeIds)
1057        {
1058            ContentType cType = _cTypeEP.getExtension(contentTypeId);
1059            if (cType != null)
1060            {
1061                Map<String, Object> contentTypeProperties = getContentTypeProperties(cType);
1062                if (withRight)
1063                {
1064                    contentTypeProperties.put("rightEvaluated", _hasRight(cType));
1065                }
1066                results.add(contentTypeProperties);
1067            }
1068        }
1069        
1070        return results;
1071    }
1072    
1073    /**
1074     * Get information on content types.<br>
1075     * 
1076     * @param ids The id of content types to retrieve
1077     * @param inherited If true, the sub-types will be also returned.
1078     * @param checkRights If true, only content types allowed for creation will
1079     *            be returned
1080     * @param includePrivate If true, the list will include the private content types. By default the list is restricted to the public content types.
1081     * @param includeMixins If true the list will include the mixins content types.
1082     * @param includeAbstract If true the list will include the abstract content types.
1083     * @return A Map with retrieved, unknown, not-allowed and private content types
1084     */
1085    @Callable
1086    public Map<String, Object> getContentTypesList (List<String> ids, boolean inherited, boolean checkRights, boolean includePrivate, boolean includeMixins, boolean includeAbstract)
1087    {
1088        // Collect content types.
1089        Collection<String> allContentTypesIds = new ArrayList<>();
1090        if (ids == null || ids.isEmpty())
1091        {
1092            allContentTypesIds = _getContentTypeEP().getExtensionsIds();
1093        }
1094        else
1095        {
1096            for (String id : ids)
1097            {
1098                _addIfNotPresent(allContentTypesIds, id);
1099    
1100                if (inherited)
1101                {
1102                    for (String subTypeId : _getContentTypeEP().getSubTypes(id))
1103                    {
1104                        _addIfNotPresent(allContentTypesIds, subTypeId);
1105                    }
1106                }
1107            }
1108        }
1109
1110        // Resolve and organize content types
1111        return _dispatchContentTypes(checkRights, includePrivate, includeMixins, includeAbstract, allContentTypesIds);
1112    }
1113
1114    private Map<String, Object> _dispatchContentTypes(boolean checkRights, boolean includePrivate, boolean includeMixins, boolean includeAbstract, Collection<String> allContentTypesIds)
1115    {
1116        List<Map<String, Object>> contentTypes = new ArrayList<>();
1117        List<String> unknownContentTypes = new ArrayList<>();
1118        List<String> noRightContentTypes = new ArrayList<>();
1119        List<String> privateContentTypes = new ArrayList<>();
1120        List<String> mixinContentTypes = new ArrayList<>();
1121        List<String> abstractContentTypes = new ArrayList<>();
1122
1123        for (String id : allContentTypesIds)
1124        {
1125            ContentType cType = _getContentTypeEP().getExtension(id);
1126
1127            if (cType != null)
1128            {
1129                if (cType.isAbstract() && !includeAbstract)
1130                {
1131                    abstractContentTypes.add(id);
1132                }
1133                else if (cType.isPrivate() && !includePrivate)
1134                {
1135                    privateContentTypes.add(id);
1136                }
1137                else if (cType.isMixin() && !includeMixins)
1138                {
1139                    mixinContentTypes.add(id);
1140                }
1141                else if (!checkRights || _hasRight(cType))
1142                {
1143                    contentTypes.add(getContentTypeProperties(cType));
1144                }
1145                else
1146                {
1147                    noRightContentTypes.add(id);
1148                }
1149            }
1150            else
1151            {
1152                unknownContentTypes.add(id);
1153            }
1154        }
1155        
1156        Map<String, Object> result = new HashMap<>();
1157
1158        result.put("contentTypes", contentTypes);
1159        result.put("noRightContentTypes", noRightContentTypes);
1160        result.put("unknownContentTypes", unknownContentTypes);
1161        result.put("privateContentTypes", privateContentTypes);
1162        result.put("mixinContentTypes", mixinContentTypes);
1163        result.put("abstractContentTypes", abstractContentTypes);
1164
1165        return result;
1166    }
1167
1168    /**
1169     * Get the content type properties
1170     * 
1171     * @param contentType The content type
1172     * @return The content type properties
1173     */
1174    public Map<String, Object> getContentTypeProperties(ContentType contentType)
1175    {
1176        Map<String, Object> infos = new HashMap<>();
1177
1178        infos.put("id", contentType.getId());
1179        infos.put("label", contentType.getLabel());
1180        infos.put("description", contentType.getDescription());
1181        infos.put("defaultTitle", contentType.getDefaultTitle());
1182        infos.put("iconGlyph", contentType.getIconGlyph());
1183        infos.put("iconDecorator", contentType.getIconDecorator());
1184        infos.put("iconSmall", contentType.getSmallIcon());
1185        infos.put("iconMedium", contentType.getMediumIcon());
1186        infos.put("iconLarge", contentType.getLargeIcon());
1187        infos.put("right", contentType.getRight());
1188        infos.put("isMultilingual", contentType.isMultilingual());
1189        infos.put("isSimple", contentType.isSimple());
1190        infos.put("isPrivate", contentType.isPrivate());
1191        infos.put("isAbstract", contentType.isAbstract());
1192        infos.put("isReferenceTable", contentType.isReferenceTable());
1193        infos.put("isMixin", contentType.isMixin());
1194        infos.put("superTypes", contentType.getSupertypeIds());
1195        infos.put("tags", contentType.getTags());
1196        infos.put("parentModelItemName", contentType.getParentAttributeDefinition()
1197                                                   .map(ContentAttributeDefinition::getPath)
1198                                                   .orElse(StringUtils.EMPTY));
1199        infos.put("viewNames", contentType.getViewNames(true));
1200        
1201        return infos;
1202    }
1203
1204    /**
1205     * Test if the current user has the right needed by the content type to create a content.
1206     * @param contentTypeId The content type id
1207     * @return true if the user has the right needed, false otherwise.
1208     */
1209    public boolean hasRight(String contentTypeId)
1210    {
1211        return Optional.ofNullable(contentTypeId)
1212            .filter(_getContentTypeEP()::hasExtension)
1213            .map(_getContentTypeEP()::getExtension)
1214            .map(this::_hasRight)
1215            .orElse(false);
1216    }
1217    
1218    /**
1219     * Test if the current user has the right needed by the content type to create a content.
1220     * @param contentType The content type
1221     * @return true if the user has the right needed, false otherwise.
1222     */
1223    protected boolean _hasRight(ContentType contentType)
1224    {
1225        boolean hasRight = false;
1226
1227        String right = contentType.getRight();
1228
1229        if (right == null)
1230        {
1231            hasRight = true;
1232        }
1233        else
1234        {
1235            UserIdentity user = _userProvider.getUser();
1236            hasRight = _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
1237        }
1238
1239        return hasRight;
1240    }
1241
1242    private void _addIfNotPresent(Collection<String> collection, String value)
1243    {
1244        if (!collection.contains(value))
1245        {
1246            collection.add(value);
1247        }
1248    }
1249
1250    /**
1251     * Get the id of content type to use for rendering
1252     * 
1253     * @param content The content
1254     * @return the id of dynamic or standard content type
1255     */
1256    public String getContentTypeIdForRendering(Content content)
1257    {
1258        String dynamicContentTypeId = getDynamicContentTypeId(content.getTypes(), content.getMixinTypes());
1259        if (dynamicContentTypeId != null)
1260        {
1261            return dynamicContentTypeId;
1262        }
1263        
1264        ContentType firstContentType = getFirstContentType(content);
1265        return firstContentType != null ? firstContentType.getId() : StringUtils.EMPTY;
1266    }
1267
1268    /**
1269     * Get the id of the dynamic content type matching to given ones
1270     * @param contentTypes The content types
1271     * @param mixinTypes The mixins
1272     * @return the id of dynamic content type or null if no dynamic content type was found
1273     */
1274    public String getDynamicContentTypeId(String[] contentTypes, String[] mixinTypes)
1275    {
1276        DynamicContentTypeDescriptor dynamicContentType = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(contentTypes, mixinTypes);
1277        if (dynamicContentType != null)
1278        {
1279            return dynamicContentType.getId();
1280        }
1281        
1282        return null;
1283    }
1284
1285    /**
1286     * Get the plugin name of content type to use for rendering
1287     * 
1288     * @param content The content
1289     * @return the plugin name of dynamic or standard content type
1290     */
1291    public String getContentTypePluginForRendering(Content content)
1292    {
1293        String dynamicContentTypePlugin = getDynamicContentTypePlugin(content.getTypes(), content.getMixinTypes());
1294        if (dynamicContentTypePlugin != null)
1295        {
1296            return dynamicContentTypePlugin;
1297        }
1298        
1299        ContentType firstContentType = getFirstContentType(content);
1300        return firstContentType != null ? firstContentType.getPluginName() : StringUtils.EMPTY;
1301    }
1302
1303    /**
1304     * Get the plugin name of the dynamic content type matching to given ones
1305     * @param contentTypes The content types
1306     * @param mixinTypes The mixins
1307     * @return the plugin name of dynamic content type or null if no dynamic content type was found
1308     */
1309    public String getDynamicContentTypePlugin(String[] contentTypes, String[] mixinTypes)
1310    {
1311        DynamicContentTypeDescriptor dynamicContentType = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(contentTypes, mixinTypes);
1312        if (dynamicContentType != null)
1313        {
1314            return dynamicContentType.getPluginName();
1315        }
1316        
1317        return null;
1318    }
1319
1320    /**
1321     * Retrieves the label of the content type.
1322     * 
1323     * @param content The content
1324     * @return the label.
1325     */
1326    public I18nizableText getContentTypeLabel(Content content)
1327    {
1328        DynamicContentTypeDescriptor dynamicContentType = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1329        if (dynamicContentType != null)
1330        {
1331            return dynamicContentType.getLabel();
1332        }
1333        
1334        ContentType firstContentType = getFirstContentType(content);
1335        return firstContentType != null ? firstContentType.getLabel() : new I18nizableText(StringUtils.EMPTY);
1336    }
1337
1338    /**
1339     * Retrieves the description of the content type.
1340     * 
1341     * @param content The content
1342     * @return the label.
1343     */
1344    public I18nizableText getContentTypeDescription(Content content)
1345    {
1346        DynamicContentTypeDescriptor dynamicContentType = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1347        if (dynamicContentType != null)
1348        {
1349            return dynamicContentType.getDescription();
1350        }
1351        
1352        ContentType firstContentType = getFirstContentType(content);
1353        return firstContentType != null ? firstContentType.getDescription() : new I18nizableText(StringUtils.EMPTY);
1354    }
1355
1356    /**
1357     * Retrieves the default title of the content type.
1358     * 
1359     * @param content The content
1360     * @return the label.
1361     */
1362    public I18nizableText getContentTypeDefaultTitle(Content content)
1363    {
1364        DynamicContentTypeDescriptor dynamicContentType = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1365        if (dynamicContentType != null)
1366        {
1367            return dynamicContentType.getDefaultTitle();
1368        }
1369
1370        ContentType firstContentType = getFirstContentType(content);
1371        return firstContentType != null ? firstContentType.getDefaultTitle() : new I18nizableText(StringUtils.EMPTY);
1372    }
1373
1374    /**
1375     * Retrieves the category of the content type.
1376     * 
1377     * @param content The content
1378     * @return the label.
1379     */
1380    public I18nizableText getContentTypeCategory(Content content)
1381    {
1382        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1383        if (dynamicCTDescriptor != null)
1384        {
1385            return dynamicCTDescriptor.getCategory();
1386        }
1387        
1388        ContentType firstContentType = getFirstContentType(content);
1389        return firstContentType != null ? firstContentType.getCategory() : new I18nizableText(StringUtils.EMPTY);
1390    }
1391    
1392    /**
1393     * Retrieves the CSS class to use as glyph icon of the content
1394     * @param content The content
1395     * @return the glyph
1396     */
1397    public String getIconGlyph(Content content)
1398    {
1399        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1400        if (dynamicCTDescriptor != null)
1401        {
1402            return dynamicCTDescriptor.getIconGlyph();
1403        }
1404        
1405        ContentType firstContentType = getFirstContentType(content);
1406        return firstContentType != null ? firstContentType.getIconGlyph() : null;
1407    }
1408    
1409    /**
1410     * Retrieves the CSS class to use as decorator above the main icon
1411     * @param content The content
1412     * @return the decorator CSS class name
1413     */
1414    public String getIconDecorator(Content content)
1415    {
1416        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1417        if (dynamicCTDescriptor != null)
1418        {
1419            return dynamicCTDescriptor.getIconDecorator();
1420        }
1421        
1422        ContentType firstContentType = getFirstContentType(content);
1423        return firstContentType != null ? firstContentType.getIconDecorator() : null;
1424    }
1425
1426    /**
1427     * Retrieves the URL of the icon without the context path.
1428     * 
1429     * @param content The content
1430     * @return the icon URL for the small image 16x16.
1431     */
1432    public String getSmallIcon(Content content)
1433    {
1434        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1435        if (dynamicCTDescriptor != null)
1436        {
1437            return dynamicCTDescriptor.getSmallIcon();
1438        }
1439        
1440        ContentType firstContentType = getFirstContentType(content);
1441        return firstContentType != null ? firstContentType.getSmallIcon() : StringUtils.EMPTY;
1442    }
1443
1444    /**
1445     * Retrieves the URL of the icon without the context path.
1446     * 
1447     * @param content The content
1448     * @return the icon URL for the medium image 32x32.
1449     */
1450    public String getMediumIcon(Content content)
1451    {
1452        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1453        if (dynamicCTDescriptor != null)
1454        {
1455            return dynamicCTDescriptor.getMediumIcon();
1456        }
1457        
1458        ContentType firstContentType = getFirstContentType(content);
1459        return firstContentType != null ? firstContentType.getMediumIcon() : StringUtils.EMPTY;
1460    }
1461
1462    /**
1463     * Retrieves the URL of the icon without the context path.
1464     * 
1465     * @param content The content
1466     * @return the icon URL for the large image 48x48.
1467     */
1468    public String getLargeIcon(Content content)
1469    {
1470        DynamicContentTypeDescriptor dynamicCTDescriptor = _getDynamicContentTypeDescriptorExtentionPoint().getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
1471        if (dynamicCTDescriptor != null)
1472        {
1473            return dynamicCTDescriptor.getLargeIcon();
1474        }
1475        
1476        ContentType firstContentType = getFirstContentType(content);
1477        return firstContentType != null ? firstContentType.getLargeIcon() : StringUtils.EMPTY;
1478    }
1479
1480    /**
1481     * Get the content type which determines the content icons and rendering
1482     * 
1483     * @param content The content
1484     * @return The main content type
1485     */
1486    public ContentType getFirstContentType(Content content)
1487    {
1488        TreeSet<ContentType> treeSet = new TreeSet<>(new ContentTypeComparator());
1489
1490        for (String id : content.getTypes())
1491        {
1492            ContentType contentType = _getContentTypeEP().getExtension(id);
1493            if (contentType != null)
1494            {
1495                treeSet.add(contentType);
1496            }
1497            else
1498            {
1499                if (getLogger().isWarnEnabled())
1500                {
1501                    getLogger().warn(String.format("Trying to get an unknown content type : '%s'.", id));
1502                }
1503            }
1504        }
1505        
1506        return !treeSet.isEmpty() ? treeSet.first() : null;
1507    }
1508    
1509    /**
1510     * Retrieves the definition for the standard "title" attribute.
1511     * @return the standard "title" attribute.
1512     */
1513    public AttributeDefinition<String> getTitleAttributeDefinition()
1514    {
1515        AttributeDefinition<String> definition = new AttributeDefinition<>();
1516        definition.setName(Content.ATTRIBUTE_TITLE);
1517        definition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_METADATA_TITLE_LABEL"));
1518        definition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_METADATA_TITLE_DESCRIPTION"));
1519        definition.setType(_contentAttributeTypeExtensionPoint.getExtension(ModelItemTypeConstants.STRING_TYPE_ID));
1520        definition.setMultiple(false);
1521        return definition;
1522    }
1523
1524    /**
1525     * Determines if the content type is an archived content type
1526     * @param cTypeId The id of content type
1527     * @return true if the content type is an org.ametys.cms.ArchivedContent
1528     */
1529    public boolean isArchivedContentType(String cTypeId)
1530    {
1531        return getAncestors(cTypeId).contains(ARCHIVED_CONTENT_TYPE);
1532    }
1533    
1534    class ContentTypeComparator implements Comparator<ContentType>
1535    {
1536        @Override
1537        public int compare(ContentType c1, ContentType c2)
1538        {
1539            I18nizableText t1 = c1.getLabel();
1540            I18nizableText t2 = c2.getLabel();
1541
1542            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
1543            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
1544
1545            int compareTo = str1.toString().compareTo(str2.toString());
1546            if (compareTo == 0)
1547            {
1548                // Content types have same keys but there are not equals, so do
1549                // not return 0 to add it in TreeSet
1550                // Indeed, in a TreeSet implementation two elements that are
1551                // equal by the method compareTo are, from the standpoint of the
1552                // set, equal
1553                return 1;
1554            }
1555            return compareTo;
1556        }
1557    }
1558
1559    private Cache<CacheKey, View> _getViewCache()
1560    {
1561        return _cacheManager.get(__VIEW_CACHE);
1562    }
1563    
1564    static final class CacheKey extends AbstractCacheKey
1565    {
1566        private CacheKey(Set<String> contentTypeIds, Set<String> mixinIds, String viewName)
1567        {
1568            super(contentTypeIds, mixinIds, viewName);
1569        }
1570
1571        static CacheKey of(Set<String> contentTypeIds, Set<String> mixinIds, String viewName)
1572        {
1573            return new CacheKey(contentTypeIds, mixinIds, viewName);
1574        }
1575    }
1576}