001/*
002 *  Copyright 2010 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.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.Set;
032import java.util.regex.Pattern;
033import java.util.stream.Collectors;
034
035import org.apache.avalon.framework.activity.Disposable;
036import org.apache.avalon.framework.component.ComponentException;
037import org.apache.avalon.framework.configuration.Configuration;
038import org.apache.avalon.framework.configuration.ConfigurationException;
039import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
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.service.ServiceException;
044import org.apache.avalon.framework.service.ServiceManager;
045import org.apache.avalon.framework.thread.ThreadSafe;
046import org.apache.cocoon.Constants;
047import org.apache.commons.lang3.RandomStringUtils;
048import org.apache.commons.lang3.StringUtils;
049import org.apache.excalibur.source.Source;
050import org.xml.sax.SAXException;
051
052import org.ametys.cms.content.references.RichTextOutgoingReferencesExtractor;
053import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
054import org.ametys.cms.contenttype.ContentTypesHelper.ViewConfigurations;
055import org.ametys.cms.contenttype.ContentTypesHelper.ViewConfigurationsByType;
056import org.ametys.cms.data.type.ModelItemTypeConstants;
057import org.ametys.cms.data.type.ModelItemTypeExtensionPoint;
058import org.ametys.cms.model.ContentRestrictedCompositeDefinition;
059import org.ametys.cms.model.parsing.ContentRestrictedCompositeDefinitionParser;
060import org.ametys.cms.model.parsing.ContentRestrictedRepeaterDefinitionParser;
061import org.ametys.cms.model.properties.ElementRefProperty;
062import org.ametys.cms.model.properties.Property;
063import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper;
064import org.ametys.cms.model.restrictions.RestrictedModelItem;
065import org.ametys.cms.model.restrictions.Restrictions;
066import org.ametys.cms.repository.Content;
067import org.ametys.cms.transformation.RichTextTransformer;
068import org.ametys.cms.transformation.docbook.DocbookOutgoingReferencesExtractor;
069import org.ametys.cms.transformation.docbook.DocbookTransformer;
070import org.ametys.plugins.repository.AmetysRepositoryException;
071import org.ametys.runtime.i18n.I18nizableText;
072import org.ametys.runtime.model.ElementDefinition;
073import org.ametys.runtime.model.Enumerator;
074import org.ametys.runtime.model.ModelHelper.ConfigurationAndPluginName;
075import org.ametys.runtime.model.ModelItem;
076import org.ametys.runtime.model.ModelItemContainer;
077import org.ametys.runtime.model.ModelItemGroup;
078import org.ametys.runtime.model.TemporaryViewReference;
079import org.ametys.runtime.model.View;
080import org.ametys.runtime.model.ViewElement;
081import org.ametys.runtime.model.ViewItem;
082import org.ametys.runtime.model.ViewItemAccessor;
083import org.ametys.runtime.model.exception.UndefinedItemPathException;
084import org.ametys.runtime.model.type.ElementType;
085import org.ametys.runtime.parameter.Validator;
086import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
087
088/**
089 * Type of content which is retrieved from a XML configuration.
090 * TODO document xml configuration
091 * ...
092 * Provides access based on rights and current workflow steps.<p>
093 * It used a configuration file with the following format:
094 * <code><br>
095 * &nbsp;&nbsp;&lt;restrict-to&gt;<br>
096 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;right type="read|write" id="RIGHT_ID"/&gt;]*
097 * &nbsp;&nbsp;&lt;!-- logical OR between several right id of the same type --&gt;<br>
098 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;workflow type="read|write" step="3"/&gt;]*
099 * &nbsp;&nbsp;&lt;!-- logical OR between several workflow step of the same type --&gt;<br>
100 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;cannot type="read|write"/&gt;]*<br>
101 * &nbsp;&nbsp;&lt;/restrict-to&gt;<br>
102 * </code>
103 */
104public class DefaultContentType extends AbstractContentTypeDescriptor implements ContentType, Contextualizable, ThreadSafe, Disposable
105{
106    /** Suffix for global validator role. */
107    protected static final String __GLOBAL_VALIDATOR_ROLE_PREFIX = "_globalvalidator";
108    
109    static Pattern __annotationNamePattern;
110
111    /** Model items */
112    protected Map<String, ModelItem> _modelItems = new LinkedHashMap<>();
113    /** The properties references as: Impacted ContentType -> local IndexingField name -> path to impacted content. */
114    protected Map<String, Map<String, List<String>>> _propertiesReferences = new HashMap<>();
115    /** The right needed to create a content of this type, or null if no right is needed. */
116    protected String _right;
117    /** The abstract property */
118    protected boolean _abstract;
119    /** Configured workflow names of the content type or its supertypes, can be empty */
120    protected Set<String> _configuredWorkflowNames;
121    /** Default workflow name. */
122    protected Optional<String> _defaultWorkflowName;
123    /** The tags */
124    protected Set<String> _tags;
125    /** The inheritable tags */
126    protected Set<String> _inheritableTags;
127    /** The parent attribute definition */
128    protected ContentAttributeDefinition _parentAttributeDefinition;
129    /** Service manager. */
130    protected ServiceManager _manager;
131    /** Avalon Context. */
132    protected Context _context;
133    /** Cocoon Context */
134    protected org.apache.cocoon.environment.Context _cocoonContext;
135    /** The restrictions helper */
136    protected ContentRestrictedModelItemHelper _restrictedModelItemHelper;
137    /** Default rich text transformer. */
138    protected RichTextTransformer _richTextTransformer;
139    /** Docbook (rich text) outgoing references extractor. */
140    protected RichTextOutgoingReferencesExtractor _richTextOutgoingReferencesExtractor;
141    /** Potential global validators. */
142    protected List<ContentValidator> _globalValidators;
143    /** Potentiel richtext updater */
144    protected RichTextUpdater _richTextUpdater;
145    /** The helper component for hierarchical simple contents */
146    protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper;
147    /** List of overridden attributes */
148    protected List<String> _overriddenModelItems = new ArrayList<>();
149    /** List of overridden views */
150    protected List<String> _overriddenViews = new ArrayList<>();
151    /** All content type's views */    
152    protected Map<String, View> _views = new LinkedHashMap<>();
153    /** All configurations of content type's views */
154    protected Map<String, ViewConfigurations> _viewConfigurations;
155    
156    /** The parser for content attribute's definitions */
157    protected ContentAttributeDefinitionParser _attributeDefinitionParser;
158    /** The parser for content compisite's definitions */
159    protected ContentRestrictedCompositeDefinitionParser _compositeDefinitionParser;
160    /** The parser for content repeater's definitions */
161    protected ContentRestrictedRepeaterDefinitionParser _repeaterDefinitionParser;
162    /** The parser for dublin core attribute's definitions */
163    protected DublinCoreAttributeDefinitionParser _dublinCoreAttributeDefinitionParser;
164    
165    // ComponentManager for validators
166    private ThreadSafeComponentManager<Validator> _validatorManager;
167    
168    // ComponentManager for Global Validators
169    private ThreadSafeComponentManager<ContentValidator> _globalValidatorsManager;
170    
171    private ContentTypeReservedAttributeNameExtensionPoint _contentTypeReservedAttributeNameExtensionPoint; 
172    
173    // ComponentManager for enumerators
174    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
175    
176    // ComponentManager for properties
177    private ThreadSafeComponentManager<Property> _propertiesManager;
178    // Roles of properties to instanciate via the propertiesManager, with the property's parent
179    private Map<String, Optional<ModelItemGroup>> _propertyRoles = new HashMap<>();
180    // Index to avoid properties with same role
181    private int _propertiesIndex;
182    
183    // Content attribute types extension point
184    private ModelItemTypeExtensionPoint _contentAttributeTypeExtensionPoint;
185    
186    // Content properties types extension point
187    private ModelItemTypeExtensionPoint _contentPropertyTypeExtensionPoint;
188    
189    private boolean _isSimple;
190
191    private boolean _isMultilingual;
192    
193    private ContentTypeOverridesExtensionPoint _contentTypeOverridesExtensionPoint;
194    
195    @Override
196    public void service(ServiceManager manager) throws ServiceException
197    {
198        super.service(manager);
199        _manager = manager;
200        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
201        _richTextOutgoingReferencesExtractor = (RichTextOutgoingReferencesExtractor) manager.lookup(DocbookOutgoingReferencesExtractor.ROLE);
202        _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE);
203        _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
204        _restrictedModelItemHelper = (ContentRestrictedModelItemHelper) manager.lookup(ContentRestrictedModelItemHelper.ROLE);
205        _contentAttributeTypeExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_CONTENT_ATTRIBUTE);
206        _contentPropertyTypeExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_CONTENT_PROPERTY);
207        _contentTypeOverridesExtensionPoint = (ContentTypeOverridesExtensionPoint) manager.lookup(ContentTypeOverridesExtensionPoint.ROLE);
208    }
209    
210    /**
211     * Get the ContentTypeReservedAttributeNameExtensionPoint instance
212     * @return the instance
213     */
214    protected ContentTypeReservedAttributeNameExtensionPoint _getContentTypeReservedAttributeNameExtensionPoint()
215    {
216        if (_contentTypeReservedAttributeNameExtensionPoint == null)
217        {
218            try
219            {
220                _contentTypeReservedAttributeNameExtensionPoint = (ContentTypeReservedAttributeNameExtensionPoint) _manager.lookup(ContentTypeReservedAttributeNameExtensionPoint.ROLE);
221            }
222            catch (ServiceException e)
223            {
224                throw new RuntimeException(e);
225            }
226        }
227        return _contentTypeReservedAttributeNameExtensionPoint;
228    }
229
230    @Override
231    public void contextualize(Context context) throws ContextException
232    {
233        _context = context;
234        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
235    }
236    
237    @Override
238    public void dispose()
239    {
240        _validatorManager.dispose();
241        _validatorManager = null;
242        
243        _globalValidatorsManager.dispose();
244        _globalValidatorsManager = null;
245        
246        _enumeratorManager.dispose();
247        _enumeratorManager = null;
248        
249        _propertiesManager.dispose();
250        _propertiesManager = null;
251    }
252
253    @Override
254    protected Configuration getRootConfiguration(Configuration configuration)
255    {
256        return configuration.getChild("content-type");
257    }
258    
259    /**
260     * Get the override configurations both from _override folder and extensions
261     * @return The list of the override configurations
262     * @throws ConfigurationException If an error occurs
263     */
264    protected List<ConfigurationAndPluginName> getOverrideConfigurations() throws ConfigurationException
265    {
266        List<ConfigurationAndPluginName> overrideConfigurations = getOverrideConfigurationsFromExtensions();
267        getOverrideConfigurationFomFolder().ifPresent(overrideConfigurations::add);
268        return overrideConfigurations;
269    }
270    
271    /**
272     * Get the override configuration from _override folder
273     * @return the optional override configuration from _override folder
274     * @throws ConfigurationException if an error occurred
275     */
276    protected Optional<ConfigurationAndPluginName> getOverrideConfigurationFomFolder() throws ConfigurationException
277    {
278        Optional<ConfigurationAndPluginName> overrideConfiguration = Optional.empty();
279        File ctFile = new File(_cocoonContext.getRealPath("/WEB-INF/param/content-types/_override/" + _id + ".xml"));
280        
281        if (ctFile.exists())
282        {
283            try (InputStream is = new FileInputStream(ctFile))
284            {
285                Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
286                overrideConfiguration = Optional.of(new ConfigurationAndPluginName(configuration, _pluginName));
287            }
288            catch (Exception ex)
289            {
290                Configuration configuration = overrideConfiguration.map(ConfigurationAndPluginName::configuration)
291                                                                   .orElse(null);
292                throw new ConfigurationException("Unable to parse overridden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", configuration, ex);
293            }
294        }
295        
296        return overrideConfiguration;
297    }
298    
299    /**
300     * Get the override configurations from extensions
301     * @return The list of the override configurations from extensions
302     * @throws ConfigurationException If an error occurs
303     */
304    protected List<ConfigurationAndPluginName> getOverrideConfigurationsFromExtensions() throws ConfigurationException
305    {
306        return _contentTypeOverridesExtensionPoint.getOverrideConfigurations(_id);
307    }
308    
309    @Override
310    public void configure(Configuration configuration) throws ConfigurationException
311    {
312        _validatorManager = new ThreadSafeComponentManager<>();
313        _validatorManager.setLogger(getLogger());
314        _validatorManager.contextualize(_context);
315        _validatorManager.service(_manager);
316        
317        _globalValidatorsManager = new ThreadSafeComponentManager<>();
318        _globalValidatorsManager.setLogger(getLogger());
319        _globalValidatorsManager.contextualize(_context);
320        _globalValidatorsManager.service(_manager);
321        
322        _enumeratorManager = new ThreadSafeComponentManager<>();
323        _enumeratorManager.setLogger(getLogger());
324        _enumeratorManager.contextualize(_context);
325        _enumeratorManager.service(_manager);
326        
327        _propertiesManager = new ThreadSafeComponentManager<>();
328        _propertiesManager.setLogger(getLogger());
329        _propertiesManager.contextualize(_context);
330        _propertiesManager.service(_manager);
331        
332        Configuration rootConfiguration = getRootConfiguration(configuration);
333        
334        _abstract = rootConfiguration.getAttributeAsBoolean("abstract", false);
335        
336        _configureSuperTypes(rootConfiguration);
337        
338        _configureLabels(rootConfiguration);
339        _configureIcons(rootConfiguration);
340        
341        _configureCSSFiles(rootConfiguration);
342        
343        // Tags
344        _tags = new HashSet<>();
345        _inheritableTags = new HashSet<>();
346        
347        if (rootConfiguration.getChild("tags", false) != null)
348        {
349            if (rootConfiguration.getChild("tags").getAttributeAsBoolean("inherited", false))
350            {
351                // Get tags from super types
352                for (String superTypeId : _superTypeIds)
353                {
354                    ContentType superType = _cTypeEP.getExtension(superTypeId);
355                    _tags.addAll(superType.getInheritableTags());
356                    _inheritableTags.addAll(superType.getInheritableTags());
357                }
358            }
359            
360            _configureLocalTags(rootConfiguration.getChild("tags"));
361        }
362        
363        // Rights
364        _right = rootConfiguration.getChild("right").getValue(null);
365        
366        _isSimple = true;
367        for (String superTypeId : _superTypeIds)
368        {
369            ContentType superType = _cTypeEP.getExtension(superTypeId);
370            if (superType == null)
371            {
372                throw new ConfigurationException("The content type '" + this.getId() + "' cannot extends the unexisting type '" + superTypeId + "'");
373            }
374            if (!superType.isSimple())
375            {
376                _isSimple = false;
377                break;
378            }
379        }
380        
381        _configureDefaultWorkflowName(rootConfiguration);
382        
383        _isMultilingual = false;
384        for (String superTypeId : _superTypeIds)
385        {
386            ContentType superType = _cTypeEP.getExtension(superTypeId);
387            if (superType.isMultilingual())
388            {
389                _isMultilingual = true;
390                break;
391            }
392        }
393        
394        // Attribute definitions
395        _configureModelItems(rootConfiguration);
396        
397        // Parent content type
398        _configureParentContentType(rootConfiguration);
399        
400        // Views configurations
401        _configureViewConfigurations(rootConfiguration);
402        
403        _checkForReservedAttributeName();
404        
405        // Global validators
406        _configureGlobalValidators (rootConfiguration);
407    }
408    
409    private void _checkForReservedAttributeName() throws ConfigurationException
410    {
411        Set<String> reservedNames = _getContentTypeReservedAttributeNameExtensionPoint().getExtensionsIds();
412        
413        List<String> usedReservedKeywords = new ArrayList<>(reservedNames);
414        usedReservedKeywords.retainAll(_modelItems.keySet());
415        
416        if (!usedReservedKeywords.isEmpty())
417        {
418            throw new ConfigurationException("In content type '" + _id + "', one or more attributes are named with a reserved keyword: " + StringUtils.join(usedReservedKeywords, ", ") 
419            + ". The reserved keywords are: {" + StringUtils.join(reservedNames, ", ") + "}");
420        }
421    }
422    
423    /**
424     * Configure attribute definitions
425     * @param mainConfig The content type configuration
426     * @throws ConfigurationException if an error occurred
427     */
428    protected void _configureModelItems (Configuration mainConfig) throws ConfigurationException
429    {
430        _attributeDefinitionParser = new ContentAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager);
431        _compositeDefinitionParser = new ContentRestrictedCompositeDefinitionParser(_contentAttributeTypeExtensionPoint);
432        _repeaterDefinitionParser = new ContentRestrictedRepeaterDefinitionParser(_contentAttributeTypeExtensionPoint);
433        _dublinCoreAttributeDefinitionParser = new DublinCoreAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager, _dcProvider);
434        
435        try
436        {
437            // First, get attributes from super type if applicable.
438            _modelItems.putAll(_contentTypesHelper.getModelItemsIndexedByName(_superTypeIds));
439        }
440        catch (IllegalArgumentException e)
441        {
442            throw new ConfigurationException("A model item is defined in several co-super-types '", mainConfig, e);
443        }
444        
445        Map<String, ConfigurationAndPluginName> modelItemConfigurations = new LinkedHashMap<>();
446        _getApplicableModelItems(new ConfigurationAndPluginName(mainConfig, _pluginName), modelItemConfigurations, false);
447        
448        List<ConfigurationAndPluginName> overrideConfigurations = getOverrideConfigurations();
449        _getApplicableModelItems(overrideConfigurations, modelItemConfigurations, true);
450        
451        // Then, parse own model items
452        _parseAllModelItems(modelItemConfigurations);
453        
454        try
455        {
456            _attributeDefinitionParser.lookupComponents();
457            _lookupProperties();
458//            _dublinCoreAttributeDefinitionParser.lookupComponents();
459        }
460        catch (Exception e)
461        {
462            throw new ConfigurationException("Unable to lookup parameter local components", mainConfig, e);
463        }
464    }
465    
466    /**
467     * Fill a map of the applicable model item configurations.
468     * @param configs the list of content type configurations.
469     * @param modelItemConfigurations the Map of attributes {@link Configuration}, indexed by name.
470     * @param allowOverride if true, encountering an attribute which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 
471     * @throws ConfigurationException if an error occurs.
472     */
473    protected void _getApplicableModelItems(List<ConfigurationAndPluginName> configs, Map<String, ConfigurationAndPluginName> modelItemConfigurations, boolean allowOverride) throws ConfigurationException 
474    {
475        for (ConfigurationAndPluginName configuration : configs)
476        {
477            _getApplicableModelItems(configuration, modelItemConfigurations, allowOverride);
478        }
479    }
480    
481    /**
482     * Fill a map of the applicable attribute configurations.
483     * @param config the content type configuration.
484     * @param modelItemConfigurations the Map of model item {@link Configuration}s, indexed by name.
485     * @param allowOverride if true, encountering a model item which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 
486     * @throws ConfigurationException if an error occurs.
487     */
488    protected void _getApplicableModelItems(ConfigurationAndPluginName config, Map<String, ConfigurationAndPluginName> modelItemConfigurations, boolean allowOverride) throws ConfigurationException
489    {
490        for (Configuration childConfiguration : config.configuration().getChildren())
491        {
492            String childName = childConfiguration.getName();
493            
494            if (childName.equals("metadata") || childName.equals("repeater") || childName.equals("property"))
495            {
496                String modelItemName = childConfiguration.getAttribute("name", "");
497                
498                if (!allowOverride && modelItemConfigurations.containsKey(modelItemName))
499                {
500                    throw new ConfigurationException("Model item with name '" + modelItemName + "' is already defined", childConfiguration);
501                }
502                else if (allowOverride && modelItemConfigurations.containsKey(modelItemName))
503                {
504                    _checkModelItemsTypeAndCardinality(modelItemConfigurations.get(modelItemName).configuration(), childConfiguration);
505                }
506                
507                if (allowOverride)
508                {
509                    this._overriddenModelItems.add(modelItemName);
510                }
511                
512                modelItemConfigurations.put(modelItemName, new ConfigurationAndPluginName(childConfiguration, config.pluginName()));
513            }
514            else if (childName.equals("dublin-core"))
515            {
516                modelItemConfigurations.put("dc", new ConfigurationAndPluginName(childConfiguration, config.pluginName()));
517            }
518        }
519    }
520    
521    public List<String> getOverriddenModelItems()
522    {
523        return this._overriddenModelItems;
524    }
525    
526    /**
527     * Check if all model item's types defined in first configuration are equals to those defined in second configuration
528     * @param modelItemConf1 The first configuration to compare
529     * @param modelItemConf2 The second configuration to compare
530     * @throws ConfigurationException if the types are not equals
531     */
532    protected void _checkModelItemsTypeAndCardinality (Configuration modelItemConf1, Configuration modelItemConf2) throws ConfigurationException
533    {
534        String type = modelItemConf1.getAttribute("type", "");
535        String overridenType = modelItemConf2.getAttribute("type", "");
536        if (!overridenType.equals(type))
537        {
538            throw new ConfigurationException("The type of model item '" + modelItemConf1.getAttribute("name") + "' (" + type + ") cannot be overridden to: " + overridenType);
539            
540        }
541        boolean cardinality = modelItemConf1.getAttributeAsBoolean("multiple", false);
542        boolean overriddenCardinality = modelItemConf2.getAttributeAsBoolean("multiple", false);
543        if (cardinality != overriddenCardinality)
544        {
545            throw new ConfigurationException("The cardinality of model item '" + modelItemConf1.getAttribute("name") + "' (" + (cardinality ? "multiple" : "not multiple") + ") defined in content type '" + _id + "', can not be overridden to cardinality: " + (overriddenCardinality ? "multiple" : "not multiple"));
546        }
547        
548        if ("composite".equals(type) || modelItemConf1.getName().equals("repeater"))
549        {
550            for (Configuration childConfig1 : modelItemConf1.getChildren())
551            {
552                String childName = childConfig1.getName();
553                if (childName.equals("metadata") || childName.equals("repeater") || childName.equals("property"))
554                {
555                    Configuration childConfig2 = null;
556                    for (Configuration conf : modelItemConf2.getChildren(childName))
557                    {
558                        if (childConfig1.getAttribute("name").equals(conf.getAttribute("name")))
559                        {
560                            childConfig2 = conf;
561                            break;
562                        }
563                    }
564                    
565                    if (childConfig2 != null)
566                    {
567                        _checkModelItemsTypeAndCardinality (childConfig1, childConfig2);
568                    }
569                }
570            }
571        }
572    }
573    
574    /**
575     * Parse all attribute configurations.
576     * @param attributeConfigurations the attribute configurations.
577     * @throws ConfigurationException if the configuration is invalid.
578     */
579    protected void _parseAllModelItems(Map<String, ConfigurationAndPluginName> attributeConfigurations) throws ConfigurationException
580    {
581        for (ConfigurationAndPluginName childConfiguration : attributeConfigurations.values())
582        {
583            String childConfigName = childConfiguration.configuration().getName();
584            
585            if (childConfigName.equals("metadata") || childConfigName.equals("repeater") || childConfigName.equals("property"))
586            {
587                ModelItem child = _parseModelItem(childConfiguration, null);
588                if (child != null)
589                {
590                    final String childName = child.getName();
591                    if (_modelItems.containsKey(childName))
592                    {
593                        _checkModelItemsTypeAndCardinality(_modelItems.get(childName), child);
594                    }
595                    
596                    _checkContentTypeSimplicity(child);
597                    _modelItems.put(childName, child);
598                }
599            }
600            else if (childConfigName.equals("dublin-core"))
601            {
602                _parseDublinCoreAttributes();
603            }
604        }
605    }
606    
607    /**
608     * Parses a model item
609     * @param itemConfigurationAndPluginName configuration of the model item to parse
610     * @param parent the parent of the model item to parse. Can be <code>null</code> if the item has no parent.
611     * @return the parsed model item
612     * @throws ConfigurationException if an error occurs while the model item is parsed
613     */
614    @SuppressWarnings("static-access")
615    protected ModelItem _parseModelItem(ConfigurationAndPluginName itemConfigurationAndPluginName, ModelItemGroup parent) throws ConfigurationException
616    {
617        ModelItem modelItem = null;
618        Configuration itemConfiguration = itemConfigurationAndPluginName.configuration();
619        final String itemConfigName = itemConfiguration.getName();
620        if (itemConfigName.equals("metadata"))
621        {
622            String typeId = itemConfiguration.getAttribute("type");
623            if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(typeId))
624            {
625                modelItem = _compositeDefinitionParser.parse(_manager, itemConfigurationAndPluginName.pluginName(), itemConfiguration, this, parent);
626            }
627            else
628            {
629                modelItem = _attributeDefinitionParser.parse(_manager, itemConfigurationAndPluginName.pluginName(), itemConfiguration, this, parent);
630            }
631        }
632        else if ("repeater".equals(itemConfigName))
633        {
634            modelItem = _repeaterDefinitionParser.parse(_manager, itemConfigurationAndPluginName.pluginName(), itemConfiguration, this, parent);
635        }
636        else if ("property".equals(itemConfigName))
637        {
638            String propertyRole = _parseProperty(itemConfiguration, ++_propertiesIndex);
639            _propertyRoles.put(propertyRole, Optional.ofNullable(parent));
640            
641            // unable to lookup to the property component yet, all properties have to be initialized at one time
642            // retrieve a null item
643        }
644        
645        if (modelItem != null && modelItem instanceof ModelItemGroup)
646        {
647            for (Configuration childConfiguration : itemConfiguration.getChildren())
648            {
649                _parseModelItem(new ConfigurationAndPluginName(childConfiguration, itemConfigurationAndPluginName.pluginName()), (ModelItemGroup) modelItem);
650            }
651        }
652        
653        return modelItem;
654    }
655    
656    /**
657     * Parses a property
658     * @param propertyConfiguration configuration of the property to parse
659     * @param propertyIndex index to use in the role to be able to use several times a property with the same class in a content-type.
660     * @return the role of the parsed property component
661     * @throws ConfigurationException if an error occurs while the property is parsed
662     */
663    @SuppressWarnings("unchecked")
664    protected String _parseProperty(Configuration propertyConfiguration, int propertyIndex) throws ConfigurationException
665    {
666        String className = propertyConfiguration.getAttribute("class", null);
667        String path = propertyConfiguration.getAttribute("path", null);
668        if (className == null && path == null)
669        {
670            throw new ConfigurationException("A property defined in content type '" + this.getId() + "' does not specifiy a class not attribute path.", propertyConfiguration);
671        }
672
673        Class<? extends Property> propertyClass;
674        String propertyRole;
675        if (className != null)
676        {
677            try
678            {
679                propertyClass = (Class<? extends Property>) Class.forName(className);
680                propertyRole = getId() + "-" + className + "-" + propertyIndex;
681            }
682            catch (Exception e)
683            {
684                throw new ConfigurationException("Unable to instanciate property for class: " + className, propertyConfiguration, e);
685            }
686        }
687        else
688        {
689            propertyClass = ElementRefProperty.class;
690            propertyRole = getId() + "-" + path + "-" + propertyIndex;
691        }
692            
693        _propertiesManager.addComponent(_pluginName, null, propertyRole, propertyClass, propertyConfiguration);
694        return propertyRole;
695    }
696    
697    /**
698     * Check if all model item types defined in first model item are equals to those defined in second one
699     * @param item1 The first item to compare
700     * @param item2 The second item to compare
701     * @throws ConfigurationException if the types are not equals
702     */
703    protected void _checkModelItemsTypeAndCardinality (ModelItem item1, ModelItem item2) throws ConfigurationException
704    {
705        if (item1 instanceof ElementDefinition elementDefinition1)
706        {
707            final String item1TypeId = elementDefinition1.getType().getId();
708            final String item2TypeId = item2.getType().getId();
709            if (!(item2 instanceof ElementDefinition elementDefinition2) || !item1TypeId.equals(item2TypeId))
710            {
711                throw new ConfigurationException("The type of model item '" + elementDefinition1.getPath() + "' (" + item1TypeId + ") defined in content type '" + _id + "', can not be overridden to type: " + item2TypeId);
712            }
713            else
714            {
715                final boolean item1Cardinality = elementDefinition1.isMultiple();
716                final boolean item2Cardinality = elementDefinition2.isMultiple();
717                
718                if (item1Cardinality != item2Cardinality)
719                {
720                    throw new ConfigurationException("The cardinality of model item '" + elementDefinition1.getPath() + "' (" + (item1Cardinality ? "multiple" : "not multiple") + ") defined in content type '" + _id + "', can not be overridden to cardinality: " + (item2Cardinality ? "multiple" : "not multiple"));
721                }
722            }
723        }
724        else
725        {
726            if (!(item2 instanceof ModelItemGroup))
727            {
728                throw new ConfigurationException("The item group '" + item1 + "' can not be overriden by the non item group '" + item2 + "' in content type '" + item2.getModel() + "'");
729            }
730
731            ModelItemGroup group1 = (ModelItemGroup) item1;
732            ModelItemGroup group2 = (ModelItemGroup) item2;
733            
734            for (ModelItem subItemfromGroup1 : group1.getChildren())
735            {
736                ModelItem subItemFromGroup2 = group2.getChild(subItemfromGroup1.getName());
737                if (subItemFromGroup2 != null)
738                {
739                    _checkModelItemsTypeAndCardinality(subItemfromGroup1, subItemFromGroup2);
740                    _checkContentTypeSimplicity(subItemfromGroup1);
741                }
742            }
743        }
744    }
745    
746    /**
747     * Checks the given model item to determine if this content type is multilingual and/or simple
748     * All items of a simple content-type have to be a simple type (string, long, date, ..)
749     * A multilingual content type should contain at least one model item of type MULTILINGUAL-STRING
750     * @param modelItem The model item to check
751     */
752    protected void _checkContentTypeSimplicity(ModelItem modelItem)
753    {
754        if (modelItem instanceof ModelItemGroup)
755        {
756            // If the content type contains groups, it is not simple
757            _isSimple = false;
758        }
759        else if (modelItem instanceof ElementDefinition elementDefinition)
760        {
761            ElementType type = elementDefinition.getType();
762            if (!type.isSimple())
763            {
764                // If there is a no simple attribute, the content type is not simple 
765                _isSimple = false;
766            }
767            
768            if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(type.getId()))
769            {
770                // If there is a multilingual-string attribute, the content type is multilingual
771                _isMultilingual = true;
772            }
773        }
774    }
775    
776    /**
777     * Parse DublinCore attributes
778     * @throws ConfigurationException if the configuration is invalid
779     */
780    @SuppressWarnings("static-access")
781    protected void _parseDublinCoreAttributes() throws ConfigurationException
782    {
783        Source src = null;
784        
785        try
786        {
787            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
788            
789            if (src.exists())
790            {
791                Configuration configuration = null;
792                try (InputStream is = src.getInputStream())
793                {
794                    configuration = new DefaultConfigurationBuilder(true).build(is);
795                }
796                
797                ContentRestrictedCompositeDefinition definition = new ContentRestrictedCompositeDefinition();
798                definition.setModel(this);
799                definition.setName("dc");
800                definition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL"));
801                definition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC"));
802                definition.setType(_contentAttributeTypeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID));
803                
804                for (Configuration childConfiguration : configuration.getChildren())
805                {
806                    String childName = childConfiguration.getName();
807                    
808                    if (childName.equals("metadata"))
809                    {
810                        // TODO TAC, à faire ?
811                        _dublinCoreAttributeDefinitionParser.parse(_manager, _pluginName, childConfiguration, this, definition);
812                    }
813                }
814                
815                _modelItems.put("dc", definition);
816            }
817        }
818        catch (IOException | SAXException e)
819        {
820            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
821        }
822        finally
823        {
824            if (src != null)
825            {
826                _srcResolver.release(src);
827            }
828        }
829    }
830    
831    /**
832     * Execute the lookup on all properties
833     * @throws ConfigurationException if the configuration is invalid.
834     */
835    @SuppressWarnings("unchecked")
836    protected void _lookupProperties() throws ConfigurationException
837    {
838        try
839        {
840            _propertiesManager.initialize();
841        }
842        catch (Exception e)
843        {
844            throw new ConfigurationException("Unable to initialize properties manager", e);
845        }
846        
847        for (String propertyRole : _propertyRoles.keySet())
848        {
849            try
850            {
851                Property property = _propertiesManager.lookup(propertyRole);
852                String propertyName = property.getName();
853                Optional<ModelItemGroup> optParent = _propertyRoles.get(propertyRole);
854                
855                property.setModel(this);
856                property.setAvailableTypeExtensionPoint(_contentPropertyTypeExtensionPoint);
857
858                // If the property's container already has an item with the same name, check item types
859                ModelItemContainer propertyContainer = optParent.map(ModelItemContainer.class::cast)
860                                                                .orElse(this);
861                if (propertyContainer.hasModelItem(propertyName))
862                {
863                    ModelItem item = propertyContainer.getModelItem(propertyName);
864                    _checkModelItemsTypeAndCardinality(item, property);
865                }
866                
867                _checkContentTypeSimplicity(property);
868
869                // Insert the property at the right place in model items hierarchy
870                optParent.ifPresentOrElse(parent -> parent.addChild(property), 
871                    () -> _modelItems.put(property.getName(), property));
872            }
873            catch (ComponentException e)
874            {
875                throw new ConfigurationException("Unable to lookup property with role: '" + propertyRole + "' for content type: " + this.getId(), e);
876            }
877        }
878    }
879    
880    /**
881     * Configure the default workflow name from the XML configuration.
882     *  - From the overriden configuration
883     *  - If not, from the current configuration
884     *  - If not, from the supertypes
885     *  - If it cannot be determined and the content type is a reference table, then "reference-table"
886     *  - Otherwise "content"
887     * @param mainConfig The configuration
888     * @throws ConfigurationException if an exception occurs
889     */
890    protected void _configureDefaultWorkflowName(Configuration mainConfig) throws ConfigurationException
891    {
892        _configuredWorkflowNames = getOverrideConfigurationFomFolder()
893            .map(ConfigurationAndPluginName::configuration)
894            // Override mode
895            .map(oc -> oc.getChild("default-workflow").getValue(null))
896            // Normal mode
897            .or(() -> Optional.ofNullable(mainConfig.getChild("default-workflow").getValue(null)))
898            // Transform it in a singleton set
899            .map(Set::of)
900            // Inherited mode
901            .orElseGet(this::_getDefaultWorkflowNamesFromSupertypes);
902        
903        _defaultWorkflowName = _configuredWorkflowNames.size() > 1
904                // Several workflow names returns undefined
905                ? Optional.empty()
906                : _configuredWorkflowNames.stream()
907                    // One workflow name is directly returned
908                    .findFirst()
909                    // Otherwise default workflow : reference-table or content
910                    .or(() -> Optional.of(isReferenceTable() ? "reference-table" : "content"));
911    }
912    
913    private Set<String> _getDefaultWorkflowNamesFromSupertypes()
914    {
915        Set<String> defaultWorkflowNames = new HashSet<>();
916        
917        // Get tags from super types
918        for (String superTypeId : _superTypeIds)
919        {
920            ContentType superType = _cTypeEP.getExtension(superTypeId);
921            defaultWorkflowNames.addAll(superType.getConfiguredDefaultWorkflowNames());
922        }
923        
924        if (defaultWorkflowNames.size() > 1)
925        {
926            getLogger().warn("Several default workflows are defined for content type '{}' : {}.", _id, StringUtils.join(defaultWorkflowNames));
927        }
928        
929        return defaultWorkflowNames;
930    }
931
932    /**
933     * Configures the "parent" content type. 
934     * This must not be confounded with the super types. (See {@link AbstractContentTypeDescriptor#_configureSuperTypes(Configuration)})
935     * @param mainConfig The main configuration
936     * @throws ConfigurationException if an error occurred
937     */
938    protected void _configureParentContentType(Configuration mainConfig) throws ConfigurationException
939    {
940        Configuration parentCTypeConf = mainConfig.getChild("parent-ref", false);
941        _parentAttributeDefinition = null;
942        
943        if (parentCTypeConf != null)
944        {
945            // Check this content type is a reference table
946            if (!isReferenceTable())
947            {
948                getLogger().error("The 'parent-ref' tag for content type '{}' is defined but this feature is only enabled for reference table content types. It will be ignored.", getId());
949                return;
950            }
951            
952            String refAttributeName = parentCTypeConf.getAttribute("name");
953            // Check valid reference of metadata
954            if (!_modelItems.containsKey(refAttributeName))
955            {
956                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it does not exist. It will be ignored.", getId(), refAttributeName);
957                return;
958            }
959            
960            ModelItem modelItem = _modelItems.get(refAttributeName);
961            // Check metadata of type "content"
962            if (!(modelItem instanceof ContentAttributeDefinition))
963            {
964                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it is not an attribute of type CONTENT. It will be ignored.", getId(), refAttributeName);
965                return;
966            }
967            
968            ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) modelItem;
969            if (contentAttributeDefinition.isMultiple())
970            {
971                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it is a multiple attribute. It will be ignored.", getId(), refAttributeName);
972                return;
973            }
974            
975            String parentCTypeName = contentAttributeDefinition.getContentTypeId();
976            ContentType parentCType = _cTypeEP.getExtension(parentCTypeName);
977            
978            if (parentCType == null)
979            {
980                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it does not exist or is not yet initialized. It will be ignored.", getId(), refAttributeName, parentCTypeName);
981                return;
982            }
983            // Check parent content type is private AND simple
984            else if (!parentCType.isPrivate())
985            {
986                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it is not private. It will be ignored.", getId(), refAttributeName, parentCTypeName);
987                return;
988            }
989            else if (!parentCType.isReferenceTable())
990            {
991                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it is not a reference table. It will be ignored.", getId(), refAttributeName, parentCTypeName);
992                return;
993            }
994            
995            if (!_hierarchicalSimpleContentsHelper.registerRelation(parentCType, this))
996            {
997                getLogger().error("The content type '{}' defines a parent hierarchy which is not valid. See previous logs to know more.\nThis error can lead to UI issues.", getId());
998                return;
999            }
1000            _parentAttributeDefinition = contentAttributeDefinition;
1001        }
1002    }
1003    
1004    /**
1005     * Configure the content type view configurations
1006     * @param mainConfig The content type configuration
1007     * @throws ConfigurationException if an error occurred
1008     */
1009    protected void _configureViewConfigurations(Configuration mainConfig) throws ConfigurationException
1010    {
1011        // Get applicable views configurations
1012
1013        // 1., get the views configured in the content type extension
1014        ConfigurationAndPluginName mainConfigurationAndPluginName = new ConfigurationAndPluginName(mainConfig, _pluginName);
1015        Map<String, ViewConfigurations> viewConfigurationsFromExtensionWithLegacySyntax = _getApplicableViewConfigurations(mainConfigurationAndPluginName, VIEW_TAG_NAME_WITH_LEGACY_SYNTAX);
1016        Map<String, ViewConfigurations> viewConfigurationsFromExtensionWithClassicSyntax = _getApplicableViewConfigurations(mainConfigurationAndPluginName, VIEW_TAG_NAME);
1017        Map<String, ViewConfigurations> viewConfigurationsFromExtension = mergeViewConfigurationsWithLegacyAndClassicSyntaxes(viewConfigurationsFromExtensionWithLegacySyntax, viewConfigurationsFromExtensionWithClassicSyntax);
1018        
1019        // 2. get the views configured in the content type overrides extensions
1020        List<ConfigurationAndPluginName> overrideConfigurationsFromExtensions = getOverrideConfigurationsFromExtensions();
1021        Map<String, ViewConfigurations> viewConfigurationsFromOverrideExtensionWithLegacySyntax = _getApplicableViewConfigurations(overrideConfigurationsFromExtensions, VIEW_TAG_NAME_WITH_LEGACY_SYNTAX);
1022        Map<String, ViewConfigurations> viewConfigurationsFromOverrideExtensionWithClassicSyntax = _getApplicableViewConfigurations(overrideConfigurationsFromExtensions, VIEW_TAG_NAME);
1023        Map<String, ViewConfigurations> viewConfigurationsFromOverrideExtension = mergeViewConfigurationsWithLegacyAndClassicSyntaxes(viewConfigurationsFromOverrideExtensionWithLegacySyntax, viewConfigurationsFromOverrideExtensionWithClassicSyntax);
1024        
1025        // Merge view configurations from extension and overrides extensions.
1026        Map<String, ViewConfigurations> viewConfigurationsFromAllExtensions = mergeViewConfigurationsWithOverrides(viewConfigurationsFromExtension, viewConfigurationsFromOverrideExtension);
1027        
1028        // 3. get the views configured in the _override folder
1029        Optional<ConfigurationAndPluginName> optOverrideConfigurationFomFolder = getOverrideConfigurationFomFolder();
1030        Map<String, ViewConfigurations> viewConfigurationsFromOverrideFolder = Map.of();
1031        if (optOverrideConfigurationFomFolder.isPresent())
1032        {
1033            ConfigurationAndPluginName overrideConfigurationFomFolder = optOverrideConfigurationFomFolder.get();
1034            Map<String, ViewConfigurations> viewConfigurationsFromOverrideFolderWithLegacySyntax = _getApplicableViewConfigurations(overrideConfigurationFomFolder, VIEW_TAG_NAME_WITH_LEGACY_SYNTAX);
1035            Map<String, ViewConfigurations> viewConfigurationsFromOverrideFolderWithClassicSyntax = _getApplicableViewConfigurations(overrideConfigurationFomFolder, VIEW_TAG_NAME);
1036            viewConfigurationsFromOverrideFolder = mergeViewConfigurationsWithLegacyAndClassicSyntaxes(viewConfigurationsFromOverrideFolderWithLegacySyntax, viewConfigurationsFromOverrideFolderWithClassicSyntax);
1037        }
1038
1039        // Merge view configurations from extensions and override folder.
1040        _viewConfigurations = mergeViewConfigurationsWithOverrides(viewConfigurationsFromAllExtensions, viewConfigurationsFromOverrideFolder);
1041    }
1042    
1043    /**
1044     * Merge view configurations with legacy and classic syntax.
1045     * Keep the main configuration from the classic syntax if there is one. Concatenate all override configurations 
1046     * @param viewConfigurationsWithLegacySyntax the view configurations with legacy syntax
1047     * @param viewConfigurationsWithClassicSyntax the view configurations with legacy syntax
1048     * @return the merged view configurations
1049     */
1050    protected Map<String, ViewConfigurations> mergeViewConfigurationsWithLegacyAndClassicSyntaxes(Map<String, ViewConfigurations> viewConfigurationsWithLegacySyntax, Map<String, ViewConfigurations> viewConfigurationsWithClassicSyntax)
1051    {
1052        Map<String, ViewConfigurations> viewConfigurations = viewConfigurationsWithLegacySyntax;
1053        for (String viewName : viewConfigurationsWithClassicSyntax.keySet())
1054        {
1055            if (viewConfigurations.containsKey(viewName))
1056            {
1057                ViewConfigurations currentViewConfigurationsWithClassicSyntax = viewConfigurationsWithClassicSyntax.get(viewName);
1058                
1059                // Keep the main configuration from the classic syntax if there is one
1060                Optional<ConfigurationAndPluginName> mainConfiguration = currentViewConfigurationsWithClassicSyntax.mainConfiguration()
1061                                                                                                      .or(() -> viewConfigurations.get(viewName).mainConfiguration());
1062                
1063                // Concatenate all override configurations
1064                List<ConfigurationAndPluginName> allOverrides = viewConfigurations.get(viewName).overrides();
1065                allOverrides.addAll(currentViewConfigurationsWithClassicSyntax.overrides());
1066
1067                viewConfigurations.put(viewName, new ViewConfigurations(viewName, mainConfiguration, allOverrides));
1068            }
1069            else
1070            {
1071                // Add the nonexistent view configurations
1072                viewConfigurations.put(viewName, viewConfigurationsWithClassicSyntax.get(viewName));
1073            }
1074        }
1075        
1076        return viewConfigurations;
1077    }
1078    
1079    /**
1080     * Merge view configurations with overrides
1081     * If there is a main configuration in overrides, do not keep configurations (main or overrides) of the original view configurations. Otherwise, concatenate all override configurations
1082     * @param viewConfigurations the original view configurations
1083     * @param viewOverrideConfigurations the override view configurations
1084     * @return @return the merged view configurations 
1085     */
1086    protected Map<String, ViewConfigurations> mergeViewConfigurationsWithOverrides(Map<String, ViewConfigurations> viewConfigurations, Map<String, ViewConfigurations> viewOverrideConfigurations)
1087    {
1088        Map<String, ViewConfigurations> mergedViewConfigurations = viewConfigurations;
1089        for (String viewName : viewOverrideConfigurations.keySet())
1090        {
1091            if (mergedViewConfigurations.containsKey(viewName))
1092            {
1093                ViewConfigurations currentViewOverrideConfigurations = viewOverrideConfigurations.get(viewName);
1094                if (currentViewOverrideConfigurations.mainConfiguration().isPresent())
1095                {
1096                    // If there is a main configuration in overrides, do not keep configurations (main or overrides) of the original view configurations
1097                    mergedViewConfigurations.put(viewName, viewOverrideConfigurations.get(viewName));
1098                }
1099                else
1100                {
1101                    // Otherwise, concatenate all override configurations
1102                    mergedViewConfigurations.get(viewName).overrides().addAll(currentViewOverrideConfigurations.overrides());
1103                }
1104            }
1105            else
1106            {
1107                // Add the nonexistent view configurations
1108                mergedViewConfigurations.put(viewName, viewOverrideConfigurations.get(viewName));
1109            }
1110        }
1111        
1112        return mergedViewConfigurations;
1113    }
1114    
1115    /**
1116     * Compute the applicable views from their configurations. (from "_override" folder and from extensions)
1117     * @param configurationAndPluginNames The content type configuration
1118     * @param viewTagName The name of the tag containing the view
1119     * @return the applicable views, indexed by their names. For each view, indicates if the view is configured 
1120     * @throws ConfigurationException if the configuration is invalid
1121     */
1122    protected Map<String, ViewConfigurations> _getApplicableViewConfigurations(List<ConfigurationAndPluginName> configurationAndPluginNames, String viewTagName) throws ConfigurationException 
1123    {
1124        Map<String, ViewConfigurations> allApplicableViewConfigurations = new LinkedHashMap<>();
1125        
1126        for (ConfigurationAndPluginName configurationAndPluginName : configurationAndPluginNames)
1127        {
1128            Map<String, ViewConfigurations> applicableViewConfigurations = _getApplicableViewConfigurations(configurationAndPluginName, viewTagName);
1129            
1130            for (String viewName : applicableViewConfigurations.keySet())
1131            {
1132                if (allApplicableViewConfigurations.containsKey(viewName))
1133                {
1134                    // Concatenate all override configurations
1135                    allApplicableViewConfigurations.get(viewName).overrides().addAll(applicableViewConfigurations.get(viewName).overrides());
1136                }
1137                else
1138                {
1139                    // Add the nonexistent view configurations
1140                    allApplicableViewConfigurations.put(viewName, applicableViewConfigurations.get(viewName));
1141                }
1142            }
1143        }
1144        
1145        return allApplicableViewConfigurations;
1146    }
1147    
1148    /**
1149     * Compute the applicable views from their configurations.
1150     * @param configurationAndPluginName The content type configuration
1151     * @param viewTagName The name of the tag containing the view
1152     * @return the applicable views, indexed by their names. For each view, indicates if the view is configured 
1153     * @throws ConfigurationException if the configuration is invalid
1154     */
1155    protected Map<String, ViewConfigurations> _getApplicableViewConfigurations(ConfigurationAndPluginName configurationAndPluginName, String viewTagName) throws ConfigurationException
1156    {
1157        Map<String, ViewConfigurations> applicableViewConfigurations = new LinkedHashMap<>();
1158
1159        for (Configuration viewConfiguration : configurationAndPluginName.configuration().getChildren(viewTagName))
1160        {
1161            String viewName = viewConfiguration.getAttribute("name");
1162            ViewConfigurations viewConfigurations = applicableViewConfigurations.computeIfAbsent(viewName, configs -> new ViewConfigurations(viewName, Optional.empty(), new ArrayList<>()));
1163            
1164            if (viewConfiguration.getAttributeAsBoolean("override", false))
1165            {
1166                viewConfigurations.overrides().add(new ConfigurationAndPluginName(viewConfiguration, configurationAndPluginName.pluginName()));
1167            }
1168            else
1169            {
1170                if (viewConfigurations.mainConfiguration().isPresent())
1171                {
1172                    // There is already a main configuration for this view in the same configuration file
1173                    throw new ConfigurationException("The view named '" + viewName + "' is defined twice in the content type '" + this.getId() + "'.", configurationAndPluginName.configuration());
1174                }
1175                else
1176                {
1177                    applicableViewConfigurations.put(viewName, new ViewConfigurations(viewName, Optional.of(new ConfigurationAndPluginName(viewConfiguration, configurationAndPluginName.pluginName())), viewConfigurations.overrides()));
1178                }
1179            }
1180        }
1181        
1182        return applicableViewConfigurations;
1183    }
1184    
1185    /**
1186     * Configure the global validators for content type
1187     * @param config The content type configuration
1188     * @throws ConfigurationException if an error occurs
1189     */
1190    protected void _configureGlobalValidators (Configuration config) throws ConfigurationException
1191    {
1192        _globalValidators = new ArrayList<>();
1193        List<String> globalValidatorsToLookup = new ArrayList<>();
1194        
1195        Configuration globalValidatorsConfig = config.getChild("global-validators", true);
1196        globalValidatorsToLookup.addAll(_parseGlobalValidators(new ConfigurationAndPluginName(globalValidatorsConfig, _pluginName), config.getAttributeAsBoolean("include-from-supertype", true)));
1197        
1198        List<ConfigurationAndPluginName> overrideConfigurationAndPluginNames = getOverrideConfigurations();
1199        // Global validators into an overriden configuration are added to the original global validators
1200        for (ConfigurationAndPluginName overrideConfigurationAndPluginName : overrideConfigurationAndPluginNames)
1201        {
1202            Configuration overrideConfiguration = overrideConfigurationAndPluginName.configuration().getChild("global-validators", true);
1203            globalValidatorsToLookup.addAll(_parseGlobalValidators(new ConfigurationAndPluginName(overrideConfiguration, overrideConfigurationAndPluginName.pluginName()), false));
1204        }
1205        
1206        try
1207        {
1208            _globalValidatorsManager.initialize();
1209        }
1210        catch (Exception e)
1211        {
1212            throw new ConfigurationException("Unable to initialize global validator manager", e);
1213        }
1214        
1215        for (String validatorRole : globalValidatorsToLookup)
1216        {
1217            try
1218            {
1219                ContentValidator contentValidator = _globalValidatorsManager.lookup(validatorRole);
1220                contentValidator.setContentType(this);
1221                
1222                _globalValidators.add(contentValidator);
1223            }
1224            catch (ComponentException e)
1225            {
1226                throw new ConfigurationException("Unable to lookup global validator role: '" + validatorRole + "' for content type: " + this.getId(), e);
1227            }
1228        }
1229        
1230    }
1231    
1232    /**
1233     * Parse the global validators
1234     * @param configurationAndPluginName the configuration
1235     * @param includeSuperTypeValidators true to include validators of super types
1236     * @return the role of global validators to be lookuped
1237     * @throws ConfigurationException if configuration is incorrect
1238     */
1239    @SuppressWarnings("unchecked")
1240    protected List<String> _parseGlobalValidators(ConfigurationAndPluginName configurationAndPluginName, boolean includeSuperTypeValidators) throws ConfigurationException
1241    {
1242        List<String> gvRoles = new ArrayList<>();
1243        
1244        if (includeSuperTypeValidators)
1245        {
1246            for (String superTypeId : _superTypeIds)
1247            {
1248                ContentType cType = _cTypeEP.getExtension(superTypeId);
1249                _globalValidators.addAll(cType.getGlobalValidators());
1250            }
1251        }
1252        
1253        for (Configuration globalValidatorConfig : configurationAndPluginName.configuration().getChildren("global-validator"))
1254        {
1255            String globalValidatorId = __GLOBAL_VALIDATOR_ROLE_PREFIX + RandomStringUtils.randomAlphanumeric(10);
1256            String validatorClassName = globalValidatorConfig.getAttribute("class");
1257            
1258            try
1259            {
1260                Class validatorClass = Class.forName(validatorClassName);
1261                _globalValidatorsManager.addComponent(configurationAndPluginName.pluginName(), null, globalValidatorId, validatorClass, globalValidatorConfig);
1262            }
1263            catch (Exception e)
1264            {
1265                throw new ConfigurationException("Unable to instantiate global validator for class: " + validatorClassName, e);
1266            }
1267            
1268            gvRoles.add(globalValidatorId);
1269        }
1270        
1271        return gvRoles;
1272    }
1273    
1274    /**
1275     * Parse the tags and add it to tags list
1276     * @param configuration the configuration to use
1277     * @throws ConfigurationException if the configuration is not valid.
1278     */
1279    protected void _configureLocalTags(Configuration configuration)  throws ConfigurationException
1280    {
1281        Configuration[] children = configuration.getChildren("tag");
1282        for (Configuration tagConfig : children)
1283        {
1284            String tagValue = tagConfig.getValue();
1285            if (tagConfig.getAttributeAsBoolean("inheritable", true))
1286            {
1287                _inheritableTags.add(tagValue);
1288            }
1289            _tags.add(tagValue);
1290        }
1291    }
1292    
1293    public void initializeAfterModelItemsInitialization() throws Exception
1294    {
1295        _checkTitleAttribute();
1296        _checkContentAttributes(this, 0);
1297        
1298        _computePropertiesReferences(this);
1299        
1300        _parseViews();
1301    }
1302    
1303    @SuppressWarnings("static-access")
1304    private void _checkTitleAttribute() throws ConfigurationException
1305    {
1306        if (!isAbstract() && !isMixin())
1307        {
1308            ModelItem titleItem = null;
1309            if (hasModelItem(Content.ATTRIBUTE_TITLE))
1310            {
1311                titleItem = getModelItem(Content.ATTRIBUTE_TITLE);
1312            }
1313            else
1314            {
1315                // The title attribute is mandatory for non abstract content types
1316                throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined.");
1317            }
1318            
1319            // The title attribute should'nt be a group item
1320            if (!(titleItem instanceof ElementDefinition))
1321            {
1322                throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined.");
1323            }
1324            
1325            // The title attribute should be a string or a multilingual string
1326            ElementDefinition titleAttribute = (ElementDefinition) titleItem;
1327            String typeId = titleAttribute.getType().getId();
1328            if (!ModelItemTypeConstants.STRING_TYPE_ID.equals(typeId) && !ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(typeId))
1329            {
1330                throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' is defined with the type '" + typeId + "'. Only '" + ModelItemTypeConstants.STRING_TYPE_ID + "' and '" + ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID + "' are allowed");
1331            }
1332            
1333            // The title attribute should not be multiple
1334            if (titleAttribute.isMultiple())
1335            {
1336                throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should not be multiple.");
1337            }
1338            
1339            // The title attribute must be mandatory
1340            Validator titleValidator = titleAttribute.getValidator();
1341            if (titleValidator == null || !(boolean) titleValidator.getConfiguration().get("mandatory"))
1342            {
1343                throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should be mandatory.");
1344            }
1345        }
1346    }
1347    
1348    /**
1349     * Check for each content attribute: the content type id, the mutual references and the default values
1350     * @param modelItemContainer the {@link ModelItemContainer} to check
1351     * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater.
1352     * @throws ConfigurationException if a content attribute has an invalid configuration
1353     */
1354    protected void _checkContentAttributes(ModelItemContainer modelItemContainer, int repeaterLevel) throws ConfigurationException
1355    {
1356        for (ModelItem modelItem : modelItemContainer.getModelItems())
1357        {
1358            if (modelItem instanceof ContentAttributeDefinition definition)
1359            {
1360                _checkContentTypeId(definition);
1361                _checkMutualReferences(definition, repeaterLevel);
1362                definition.checkDefaultValue();
1363            }
1364            
1365            // Check sub-attributes
1366            if (modelItem instanceof ModelItemContainer)
1367            {
1368                // Increment the repeater level if the current attribute is a repeater.
1369                int newRepeaterLevel = (modelItem instanceof org.ametys.plugins.repository.model.RepeaterDefinition) ? repeaterLevel + 1 : repeaterLevel;
1370                _checkContentAttributes((ModelItemContainer) modelItem, newRepeaterLevel);
1371            }
1372        }
1373    }
1374    
1375    /**
1376     * Check the content type id of the given content attribute definition
1377     * @param definition the definition to check
1378     * @throws ConfigurationException if the given content attribute references an invalid or non-existing content type.
1379     */
1380    protected void _checkContentTypeId(ContentAttributeDefinition definition) throws ConfigurationException
1381    {
1382        String contentTypeId = definition.getContentTypeId();
1383        
1384        if (StringUtils.isNotBlank(contentTypeId) && !_cTypeEP.hasExtension(contentTypeId))
1385        {
1386            throw new ConfigurationException("The content attribute of path " + definition.getPath() + " in content type " + getId() + " references a nonexistent content-type: '" + contentTypeId + "'");
1387        }
1388    }
1389    
1390    /**
1391     * Check the mutual reference declaration of the given content attribute definition
1392     * @param definition the definition to check
1393     * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater.
1394     * @throws ConfigurationException if there is a problem with mutual reference declaration
1395     */
1396    protected void _checkMutualReferences(ContentAttributeDefinition definition, int repeaterLevel) throws ConfigurationException
1397    {
1398        String contentTypeId = definition.getContentTypeId();
1399        String invertRelationPath = definition.getInvertRelationPath();
1400
1401        if (StringUtils.isNotEmpty(invertRelationPath))
1402        {
1403            String currentAttributePath = definition.getPath();
1404
1405            if (StringUtils.isEmpty(contentTypeId))
1406            {
1407                throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, a content type is required.");
1408            }
1409
1410            // Do not check the extension existence, this is already done in _checkContentAttribute method
1411            ContentType contentType = _cTypeEP.getExtension(contentTypeId);
1412
1413            if (repeaterLevel > 0 && definition.isMultiple())
1414            {
1415                throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, it can't be multiple AND in a repeater.");
1416            }
1417            else if (repeaterLevel >= 2)
1418            {
1419                throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, it can't be in two levels of repeaters.");
1420            }
1421
1422            try
1423            {
1424                ModelItem invertRelationDefinition = contentType.getModelItem(invertRelationPath);
1425
1426                // Ensure that the referenced attribute is of type content.
1427                if (!ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(invertRelationDefinition.getType().getId()))
1428                {
1429                    throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' of type " + contentTypeId + " is not of type Content.");
1430                }
1431
1432                String invertCTypeId = ((ContentAttributeDefinition) invertRelationDefinition).getContentTypeId();
1433                String invertPath = ((ContentAttributeDefinition) invertRelationDefinition).getInvertRelationPath();
1434
1435                // Ensure that the referenced attribute's content type is compatible with the current type.
1436                if (!_cTypeEP.isSameOrDescendant(getId(), invertCTypeId))
1437                {
1438                    throw new ConfigurationException("Mutual relationship: the attribute at path " + invertRelationPath + " of type " + contentTypeId + " references an incompatible type: " + StringUtils.defaultString(invertCTypeId, "<null>"));
1439                }
1440
1441                // Ensure that the referenced attribute references this attribute.
1442                if (!currentAttributePath.equals(invertPath))
1443                {
1444                    throw new ConfigurationException("Mutual relationship: the attribute at path " + currentAttributePath + " of type " + getId() + " references the attribute '" + invertRelationPath + "' of type " + contentTypeId + " but the latter does not reference it back.");
1445                }
1446            }
1447            catch (UndefinedItemPathException e)
1448            {
1449                // Ensure the referenced attribute presence.
1450                throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' doesn't exist for type " + contentTypeId);
1451            }
1452        }
1453    }
1454    
1455    /**
1456     * Parses the content type views
1457     * @throws ConfigurationException if an error occurred
1458     */
1459    protected void _parseViews() throws ConfigurationException
1460    {
1461        Map<String, ViewConfigurationsByType> allViewConfigurations = _mergeViewConfigurationsFromCurrentTypeAndSuperTypes();
1462        for (String viewName : allViewConfigurations.keySet())
1463        {
1464            ViewConfigurationsByType viewConfigurationsByType = allViewConfigurations.get(viewName);
1465            View view = _contentTypesParserHelper.parseView(this, viewConfigurationsByType);
1466            _views.put(viewName, view);
1467        }
1468        
1469        // Check that mandatory views aren't missing
1470        if (!_abstract && !hasTag(TAG_MIXIN))
1471        {
1472            if (!_views.containsKey("details"))
1473            {
1474                throw new ConfigurationException("Mandatory view named 'details' is missing for content type " + _id);
1475            }
1476            if (!_views.containsKey("main"))
1477            {
1478                throw new ConfigurationException("Mandatory view named 'main' is missing for content type " + _id);
1479            }
1480        }
1481    }
1482    
1483    /**
1484     * Retrieves the merge of the view configurations between the current content type and its super types 
1485     * @return the merge of the view configurations
1486     * @throws ConfigurationException if an error occurs
1487     */
1488    protected Map<String, ViewConfigurationsByType> _mergeViewConfigurationsFromCurrentTypeAndSuperTypes() throws ConfigurationException
1489    {
1490        Map<String, ViewConfigurationsByType> applicableViewConfigurationsByType = new HashMap<>();
1491        
1492        for (String viewName : _viewConfigurations.keySet())
1493        {
1494            ViewConfigurations currentTypeViewConfigurations = _viewConfigurations.get(viewName);
1495            Map<ContentType, ConfigurationAndPluginName> mainConfiguration = currentTypeViewConfigurations.mainConfiguration().isPresent()
1496                    ? Map.of(this, currentTypeViewConfigurations.mainConfiguration().get())
1497                    : Map.of();
1498            Map<ContentType, List<ConfigurationAndPluginName>> overrides = !currentTypeViewConfigurations.overrides().isEmpty()
1499                    ? Map.of(this, currentTypeViewConfigurations.overrides())
1500                    : Map.of();
1501            ViewConfigurationsByType currentTypeViewConfigurationsByType = new ViewConfigurationsByType(viewName, mainConfiguration, overrides);
1502            applicableViewConfigurationsByType.put(viewName, currentTypeViewConfigurationsByType);
1503        }
1504        
1505        Map<String, ViewConfigurationsByType> superTypesApplicableViewConfigurations = _contentTypesHelper.getViewConfigurations(_superTypeIds, new String[0]);
1506        for (String viewName : superTypesApplicableViewConfigurations.keySet())
1507        {
1508            if (!_viewConfigurations.containsKey(viewName) || _viewConfigurations.get(viewName).mainConfiguration().isEmpty())
1509            {
1510                ViewConfigurationsByType superTypeViewConfigurations = superTypesApplicableViewConfigurations.get(viewName);
1511                if (_viewConfigurations.containsKey(viewName))
1512                {
1513                    superTypeViewConfigurations.overrides()
1514                                               .put(this, _viewConfigurations.get(viewName).overrides());
1515                }
1516                
1517                applicableViewConfigurationsByType.put(viewName, superTypeViewConfigurations);
1518            }
1519        }
1520        
1521        return applicableViewConfigurationsByType;
1522    }
1523    
1524    @Override
1525    public void initializeAfterViewsInitialization() throws Exception
1526    {
1527        _resolveViewReferences();
1528    }
1529    
1530    /**
1531     * Resolve the temporary view references
1532     * @throws ConfigurationException if a view reference has a configuration error (the content attribute nesting the reference does not specify any content type, the view does not exist, ...)
1533     */
1534    protected void _resolveViewReferences() throws ConfigurationException
1535    {
1536        for (String viewName : getViewNames())
1537        {
1538            View view = getView(viewName);
1539            _resolveViewReferences(view, viewName);
1540        }
1541    }
1542    
1543    /**
1544     * Resolve the temporary view references in the given {@link ViewItemAccessor}
1545     * @param viewItemAccessor the {@link ViewItemAccessor}
1546     * @param currentViewName the name of the current view (to avoid views referencing themselves)
1547     * @throws ConfigurationException if a view reference has a configuration error (the content attribute nesting the reference does not specify any content type, the view does not exist, ...)
1548     */
1549    protected void _resolveViewReferences(ViewItemAccessor viewItemAccessor, String currentViewName) throws ConfigurationException
1550    {
1551        boolean hasResolvedReferences = false;
1552        List<ViewItem> viewItemsWithResolvedReferences = new ArrayList<>();
1553        for (ViewItem viewItem : viewItemAccessor.getViewItems())
1554        {
1555            if (viewItem instanceof TemporaryViewReference)
1556            {
1557                assert viewItemAccessor instanceof ViewElement;
1558                
1559                ElementDefinition definition = ((ViewElement) viewItemAccessor).getDefinition();
1560                assert definition instanceof ContentAttributeDefinition;
1561                
1562                String contentTypeId = ((ContentAttributeDefinition) definition).getContentTypeId();
1563                if (contentTypeId != null)
1564                {
1565                    Set<String> ancestors = _contentTypesHelper.getAncestors(this.getId());
1566                    if (!(ancestors.contains(contentTypeId) && currentViewName.equals(viewItem.getName())))
1567                    {
1568                        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
1569                        View view = contentType.getView(viewItem.getName());
1570                        if (view != null)
1571                        {
1572                            hasResolvedReferences = true;
1573                            viewItemsWithResolvedReferences.addAll(view.getViewItems());
1574                        }
1575                        else
1576                        {
1577                            throw new ConfigurationException("The view '" + viewItem.getName() + "' does not exist in content type '" + contentTypeId + "' referenced by the attribute named '" + definition.getName() + "'.");
1578                        }
1579                    }
1580                    else
1581                    {
1582                        throw new ConfigurationException("The view '" + viewItem.getName() + "' cannot make a reference to itself.");
1583                    }
1584                }
1585                else
1586                {
1587                    throw new ConfigurationException("The attribute '" + definition.getName() + "' doesn't reference any content type. It is not possible to add the items of the view '" + viewItem.getName() + "'.");
1588                }
1589            }
1590            else
1591            {
1592                if (viewItem instanceof ViewItemAccessor)
1593                {
1594                    _resolveViewReferences((ViewItemAccessor) viewItem, currentViewName);
1595                }
1596                viewItemsWithResolvedReferences.add(viewItem);
1597            }
1598        }
1599        
1600        if (hasResolvedReferences)
1601        {
1602            viewItemAccessor.clear();
1603            viewItemAccessor.addViewItems(viewItemsWithResolvedReferences);
1604        }
1605    }
1606    
1607    /**
1608     * Browses the model items of the given {@link ModelItemContainer} and computes properties references.
1609     * @param modelItemContainer the model item container
1610     * @throws ConfigurationException if a property references a model item that does not exist
1611     */
1612    protected void _computePropertiesReferences(ModelItemContainer modelItemContainer) throws ConfigurationException
1613    {
1614        for (ModelItem modelItem : modelItemContainer.getModelItems())
1615        {
1616            if (modelItem instanceof ElementRefProperty elementRefProperty)
1617            {
1618                String itemPath = elementRefProperty.getElementPath();
1619                
1620                if (!hasModelItem(itemPath) || !(getModelItem(itemPath) instanceof ElementDefinition))
1621                {
1622                    throw new ConfigurationException("Property for path '" + itemPath + "' does not correspond to an attribute of this content type");
1623                }
1624
1625                List<ModelItem> referencedModelItems = _contentTypesHelper.getModelItemPath(itemPath, this);
1626                
1627                List<String> joinPaths = new ArrayList<>();
1628                boolean localContentType = true;
1629                StringBuilder currentContentPath = new StringBuilder();
1630                for (ModelItem referencedModelItem : referencedModelItems)
1631                {
1632                    if (currentContentPath.length() > 0)
1633                    {
1634                        currentContentPath.append(ModelItem.ITEM_PATH_SEPARATOR);
1635                    }
1636                    currentContentPath.append(referencedModelItem.getName());
1637                    
1638                    if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(referencedModelItem.getType().getId()))
1639                    {
1640                        if (!localContentType)
1641                        {
1642                            joinPaths.add(currentContentPath.toString());
1643                            currentContentPath.setLength(0);
1644                            
1645                            Map<String, List<String>> cTypeRefs = _propertiesReferences.computeIfAbsent(referencedModelItem.getModel().getId(), __ -> new LinkedHashMap<>());
1646                            cTypeRefs.put(elementRefProperty.getPath(), new ArrayList<>(joinPaths));
1647                        }
1648                        
1649                        localContentType = false;
1650                    }
1651                }
1652            }
1653            else if (modelItem instanceof ModelItemGroup group)
1654            {
1655                _computePropertiesReferences(group);
1656            }
1657        }
1658    }
1659    
1660    @Override
1661    public List<ContentValidator> getGlobalValidators()
1662    {
1663        return Collections.unmodifiableList(_globalValidators);
1664    }
1665    
1666    @Override
1667    public RichTextUpdater getRichTextUpdater()
1668    {
1669        return _richTextUpdater;
1670    }
1671    
1672    @Override
1673    public Set<String> getTags()
1674    {
1675        return Collections.unmodifiableSet(_tags);
1676    }
1677    
1678    @Override
1679    public Set<String> getInheritableTags()
1680    {
1681        return Collections.unmodifiableSet(_inheritableTags);
1682    }
1683    
1684    @Override
1685    public boolean hasTag(String tagName)
1686    {
1687        return _tags.contains(tagName);
1688    }
1689    
1690    @Override
1691    public boolean isPrivate()
1692    {
1693        return hasTag(TAG_PRIVATE);
1694    }
1695    
1696    @Override
1697    public boolean isAbstract()
1698    {
1699        return _abstract;
1700    }
1701    
1702    @Override
1703    public boolean isSimple()
1704    {
1705        return _isSimple;
1706    }
1707
1708    @Override
1709    public boolean isReferenceTable()
1710    {
1711        return hasTag(TAG_REFERENCE_TABLE) || hasTag(TAG_RENDERABLE_FERENCE_TABLE);
1712    }
1713    
1714    @Override
1715    public boolean isMultilingual()
1716    {
1717        return _isMultilingual;
1718    }
1719    
1720    @Override
1721    public boolean isMixin()
1722    {
1723        return hasTag(TAG_MIXIN);
1724    }
1725    
1726    @Override
1727    public Set<String> getConfiguredDefaultWorkflowNames()
1728    {
1729        return _configuredWorkflowNames;
1730    }
1731    
1732    @Override
1733    public Optional<String> getDefaultWorkflowName()
1734    {
1735        return _defaultWorkflowName;
1736    }
1737    
1738    @Override
1739    public String getRight()
1740    {
1741        return _right;
1742    }
1743    
1744    @Override
1745    public void saxContentTypeAdditionalData(org.xml.sax.ContentHandler contentHandler, Content content) throws AmetysRepositoryException, SAXException
1746    {
1747        // Nothing
1748    }
1749    
1750    @Override
1751    public Map<String, Object> getAdditionalData(Content content)
1752    {
1753        return new HashMap<>();
1754    }
1755    
1756    @Override
1757    public String toString()
1758    {
1759        return "'" + getId() + "'";
1760    }
1761    
1762    /**
1763     * Restricted definition.
1764     * @deprecated use {@link RestrictedModelItem} instead
1765     */
1766    @Deprecated
1767    protected interface RestrictedDefinition
1768    {
1769        /**
1770         * Provides the restrictions.
1771         * @return the restrictions.
1772         */
1773        Restrictions getRestrictions();
1774    }
1775    
1776    /**
1777     * Definition with semantic annotations
1778     */
1779    protected interface AnnotableDefinition
1780    {
1781        /**
1782         * Provides the semantic annotations
1783         * @return the semantic annotations
1784         */
1785        List<SemanticAnnotation> getSemanticAnnotations();
1786
1787        /**
1788         * Set the semantic annotations
1789         * @param annotations the semantic annotations to set
1790         */
1791        void setSemanticAnnotations(List<SemanticAnnotation> annotations);
1792    }
1793    
1794    public Optional<ContentAttributeDefinition> getParentAttributeDefinition()
1795    {
1796        return Optional.ofNullable(_parentAttributeDefinition);
1797    }
1798    
1799    public Collection<ModelItem> getModelItems()
1800    {
1801        return Collections.unmodifiableCollection(_modelItems.values());
1802    }
1803    
1804    public Map<String, Map<String, List<String>>> getPropertiesReferences()
1805    {
1806        return _propertiesReferences;
1807    }
1808    
1809    public Set<String> getViewNames(boolean includeInternals)
1810    {
1811        if (includeInternals)
1812        {
1813            return Collections.unmodifiableSet(_views.keySet());
1814        }
1815        else
1816        {
1817            return _views.entrySet()
1818                         .stream()
1819                         .filter(entry -> !entry.getValue().isInternal())
1820                         .map(Map.Entry::getKey)
1821                         .collect(Collectors.toSet());
1822        }
1823    }
1824    
1825    public View getView(String viewName)
1826    {
1827        return _views.get(viewName);
1828    }
1829    
1830    public Map<String, ViewConfigurations> getViewConfigurations()
1831    {
1832        return _viewConfigurations;
1833    }
1834    
1835    /**
1836     * Get the overridden views list
1837     *  @return the overridden views list
1838     */
1839    public List<String> getOverriddenViews()
1840    {
1841        return this._overriddenViews;
1842    }
1843    
1844    public Optional<ViewConfigurations> getViewConfigurations(String viewName)
1845    {
1846        return Optional.ofNullable(_viewConfigurations.get(viewName));
1847    }
1848    
1849    public String getFamilyId()
1850    {
1851        return ContentTypeExtensionPoint.ROLE;
1852    }
1853}