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.DefaultConfiguration;
040import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
041import org.apache.avalon.framework.context.Context;
042import org.apache.avalon.framework.context.ContextException;
043import org.apache.avalon.framework.context.Contextualizable;
044import org.apache.avalon.framework.service.ServiceException;
045import org.apache.avalon.framework.service.ServiceManager;
046import org.apache.avalon.framework.thread.ThreadSafe;
047import org.apache.cocoon.Constants;
048import org.apache.commons.lang3.RandomStringUtils;
049import org.apache.commons.lang3.StringUtils;
050import org.apache.excalibur.source.Source;
051import org.xml.sax.SAXException;
052
053import org.ametys.cms.content.references.RichTextOutgoingReferencesExtractor;
054import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
055import org.ametys.cms.contenttype.indexing.CustomIndexingField;
056import org.ametys.cms.contenttype.indexing.CustomMetadataIndexingField;
057import org.ametys.cms.contenttype.indexing.DefaultMetadataIndexingField;
058import org.ametys.cms.contenttype.indexing.IndexingField;
059import org.ametys.cms.contenttype.indexing.IndexingModel;
060import org.ametys.cms.contenttype.indexing.MetadataIndexingField;
061import org.ametys.cms.contenttype.indexing.SemanticAnnotationIndexingField;
062import org.ametys.cms.data.type.ModelItemTypeConstants;
063import org.ametys.cms.model.ContentRestrictedCompositeDefinition;
064import org.ametys.cms.model.ContentRestrictedRepeaterDefinition;
065import org.ametys.cms.model.parsing.ContentRestrictedCompositeDefinitionParser;
066import org.ametys.cms.model.parsing.ContentRestrictedRepeaterDefinitionParser;
067import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper;
068import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper.FirstRestrictionsChecksState;
069import org.ametys.cms.model.restrictions.RestrictedModelItem;
070import org.ametys.cms.model.restrictions.Restrictions;
071import org.ametys.cms.repository.Content;
072import org.ametys.cms.repository.ContentAttributeTypeExtensionPoint;
073import org.ametys.cms.transformation.RichTextTransformer;
074import org.ametys.cms.transformation.docbook.DocbookOutgoingReferencesExtractor;
075import org.ametys.cms.transformation.docbook.DocbookTransformer;
076import org.ametys.plugins.repository.AmetysRepositoryException;
077import org.ametys.runtime.i18n.I18nizableText;
078import org.ametys.runtime.model.ElementDefinition;
079import org.ametys.runtime.model.Enumerator;
080import org.ametys.runtime.model.ModelItem;
081import org.ametys.runtime.model.ModelItemContainer;
082import org.ametys.runtime.model.ModelItemGroup;
083import org.ametys.runtime.model.TemporaryViewReference;
084import org.ametys.runtime.model.View;
085import org.ametys.runtime.model.ViewElement;
086import org.ametys.runtime.model.ViewItem;
087import org.ametys.runtime.model.ViewItemAccessor;
088import org.ametys.runtime.model.exception.UndefinedItemPathException;
089import org.ametys.runtime.model.type.ElementType;
090import org.ametys.runtime.parameter.AbstractParameterParser;
091import org.ametys.runtime.parameter.StaticEnumerator;
092import org.ametys.runtime.parameter.Validator;
093import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
094
095import com.google.common.collect.HashMultimap;
096import com.google.common.collect.Multimap;
097
098/**
099 * Type of content which is retrieved from a XML configuration.
100 * TODO document xml configuration
101 * ...
102 * Provides access based on rights and current workflow steps.<p>
103 * It used a configuration file with the following format:
104 * <code><br>
105 * &nbsp;&nbsp;&lt;restrict-to&gt;<br>
106 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;right type="read|write" id="RIGHT_ID"/&gt;]*
107 * &nbsp;&nbsp;&lt;!-- logical OR between several right id of the same type --&gt;<br>
108 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;workflow type="read|write" step="3"/&gt;]*
109 * &nbsp;&nbsp;&lt;!-- logical OR between several workflow step of the same type --&gt;<br>
110 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;cannot type="read|write"/&gt;]*<br>
111 * &nbsp;&nbsp;&lt;/restrict-to&gt;<br>
112 * </code>
113 */
114public class DefaultContentType extends AbstractContentTypeDescriptor implements ContentType, Contextualizable, ThreadSafe, Disposable
115{
116    /** Suffix for global validator role. */
117    protected static final String __GLOBAL_VALIDATOR_ROLE_PREFIX = "_globalvalidator";
118    
119    static Pattern __annotationNamePattern;
120
121    /** Metadata definitions. */
122    @Deprecated
123    protected Map<String, MetadataDefinition> _metadata = new LinkedHashMap<>();
124    /** Model items */
125    protected Map<String, ModelItem> _modelItems = new LinkedHashMap<>();
126    /** The right needed to create a content of this type, or null if no right is needed. */
127    protected String _right;
128    /** The abstract property */
129    protected boolean _abstract;
130    /** Configured workflow names of the content type or its supertypes, can be empty */
131    protected Set<String> _configuredWorkflowNames;
132    /** Default workflow name. */
133    protected Optional<String> _defaultWorkflowName;
134    /** The tags */
135    protected Set<String> _tags;
136    /** The inheritable tags */
137    protected Set<String> _inheritableTags;
138    /** The parent attribute definition */
139    protected ContentAttributeDefinition _parentAttributeDefinition;
140    /** Service manager. */
141    protected ServiceManager _manager;
142    /** Avalon Context. */
143    protected Context _context;
144    /** Cocoon Context */
145    protected org.apache.cocoon.environment.Context _cocoonContext;
146    /** The restrictions helper */
147    protected ContentRestrictedModelItemHelper _restrictedModelItemHelper;
148    /** Default rich text transformer. */
149    protected RichTextTransformer _richTextTransformer;
150    /** Docbook (rich text) outgoing references extractor. */
151    protected RichTextOutgoingReferencesExtractor _richTextOutgoingReferencesExtractor;
152    /** Potential global validators. */
153    protected List<ContentValidator> _globalValidators;
154    /** Potentiel richtext updater */
155    protected RichTextUpdater _richTextUpdater;
156    /** Indexing model */
157    protected IndexingModel _indexingModel;
158    /** The helper component for hierarchical simple contents */
159    protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper;
160    /** List of overridden attributes */
161    protected List<String> _overriddenAttributes = new ArrayList<>();
162    /** List of overridden views */
163    protected List<String> _overriddenViews = new ArrayList<>();
164    /** All content type's views */    
165    protected Map<String, View> _views = new LinkedHashMap<>();
166    /** All configurations of content type's views */ 
167    protected Map<String, Configuration> _viewConfigurations;
168    
169    /** The parser for content attribute's definitions */
170    protected ContentAttributeDefinitionParser _attributeDefinitionParser;
171    /** The parser for content compisite's definitions */
172    protected ContentRestrictedCompositeDefinitionParser _compositeDefinitionParser;
173    /** The parser for content repeater's definitions */
174    protected ContentRestrictedRepeaterDefinitionParser _repeaterDefinitionParser;
175    /** The parser for dublin core attribute's definitions */
176    protected DublinCoreAttributeDefinitionParser _dublinCoreAttributeDefinitionParser;
177    
178    /**
179     * ComponentManager pour les Validator
180     * @deprecated use {@link #_validatorManager} instead
181     */
182    @Deprecated
183    private ThreadSafeComponentManager<Validator> _oldValidatorManager;
184    
185    // ComponentManager for validators
186    private ThreadSafeComponentManager<Validator> _validatorManager;
187    
188    // ComponentManager pour les Global Validators
189    private ThreadSafeComponentManager<ContentValidator> _globalValidatorsManager;
190    
191    private ContentTypeReservedAttributeNameExtensionPoint _contentTypeReservedAttributeNameExtensionPoint; 
192    
193    /**
194     * ComponentManager pour les Enumerator
195     * @deprecated use {@link #_enumeratorManager} instead
196     */
197    @Deprecated
198    private ThreadSafeComponentManager<org.ametys.runtime.parameter.Enumerator> _oldEnumeratorManager;
199    
200    // ComponentManager for enumerators
201    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
202    
203    // ComponentManager pour les CustomIndexingField
204    private ThreadSafeComponentManager<CustomIndexingField> _customFieldManager;
205
206    // ComponentManager pour les CustomMetadataIndexingField
207    private ThreadSafeComponentManager<CustomMetadataIndexingField> _customMetadataIndexingFieldManager;
208    
209    // Content attribute types extesion point
210    private ContentAttributeTypeExtensionPoint _contentAttributeTypeExtensionPoint;
211    
212    private boolean _isSimple;
213
214    private boolean _isMultilingual;
215    
216    @Override
217    public void service(ServiceManager manager) throws ServiceException
218    {
219        super.service(manager);
220        _manager = manager;
221        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
222        _richTextOutgoingReferencesExtractor = (RichTextOutgoingReferencesExtractor) manager.lookup(DocbookOutgoingReferencesExtractor.ROLE);
223        _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE);
224        _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
225        _restrictedModelItemHelper = (ContentRestrictedModelItemHelper) manager.lookup(ContentRestrictedModelItemHelper.ROLE);
226        _contentAttributeTypeExtensionPoint = (ContentAttributeTypeExtensionPoint) manager.lookup(ContentAttributeTypeExtensionPoint.ROLE);
227    }
228    
229    /**
230     * Get the ContentTypeReservedAttributeNameExtensionPoint instance
231     * @return the instance
232     */
233    protected ContentTypeReservedAttributeNameExtensionPoint _getContentTypeReservedAttributeNameExtensionPoint()
234    {
235        if (_contentTypeReservedAttributeNameExtensionPoint == null)
236        {
237            try
238            {
239                _contentTypeReservedAttributeNameExtensionPoint = (ContentTypeReservedAttributeNameExtensionPoint) _manager.lookup(ContentTypeReservedAttributeNameExtensionPoint.ROLE);
240            }
241            catch (ServiceException e)
242            {
243                throw new RuntimeException(e);
244            }
245        }
246        return _contentTypeReservedAttributeNameExtensionPoint;
247    }
248
249    @Override
250    public void contextualize(Context context) throws ContextException
251    {
252        _context = context;
253        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
254    }
255    
256    @Override
257    public void dispose()
258    {
259        _oldValidatorManager.dispose();
260        _oldValidatorManager = null;
261        
262        _validatorManager.dispose();
263        _validatorManager = null;
264        
265        _globalValidatorsManager.dispose();
266        _globalValidatorsManager = null;
267        
268        _oldEnumeratorManager.dispose();
269        _oldEnumeratorManager = null;
270        
271        _enumeratorManager.dispose();
272        _enumeratorManager = null;
273        
274        _customFieldManager.dispose();
275        _customFieldManager = null;
276        
277        _customMetadataIndexingFieldManager.dispose();
278        _customMetadataIndexingFieldManager = null;
279    }
280
281    @Override
282    protected Configuration getRootConfiguration(Configuration configuration)
283    {
284        return configuration.getChild("content-type");
285    }
286    
287    @Override
288    protected Configuration getOverridenConfiguration() throws ConfigurationException
289    {
290        Configuration overridenConf = null;
291        File ctFile = new File(_cocoonContext.getRealPath("/WEB-INF/param/content-types/_override/" + _id + ".xml"));
292        
293        if (ctFile.exists())
294        {
295            try (InputStream is = new FileInputStream(ctFile))
296            {
297                 
298                overridenConf = new DefaultConfigurationBuilder(true).build(is);
299            }
300            catch (Exception ex)
301            {
302                throw new ConfigurationException("Unable to parse overriden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", overridenConf, ex);
303            }
304        }
305        
306        return overridenConf;
307    }
308    
309    @Override
310    public void configure(Configuration configuration) throws ConfigurationException
311    {
312        _oldValidatorManager = new ThreadSafeComponentManager<>();
313        _oldValidatorManager.setLogger(getLogger());
314        _oldValidatorManager.contextualize(_context);
315        _oldValidatorManager.service(_manager);
316        
317        _validatorManager = new ThreadSafeComponentManager<>();
318        _validatorManager.setLogger(getLogger());
319        _validatorManager.contextualize(_context);
320        _validatorManager.service(_manager);
321        
322        _globalValidatorsManager = new ThreadSafeComponentManager<>();
323        _globalValidatorsManager.setLogger(getLogger());
324        _globalValidatorsManager.contextualize(_context);
325        _globalValidatorsManager.service(_manager);
326        
327        _oldEnumeratorManager = new ThreadSafeComponentManager<>();
328        _oldEnumeratorManager.setLogger(getLogger());
329        _oldEnumeratorManager.contextualize(_context);
330        _oldEnumeratorManager.service(_manager);
331        
332        _enumeratorManager = new ThreadSafeComponentManager<>();
333        _enumeratorManager.setLogger(getLogger());
334        _enumeratorManager.contextualize(_context);
335        _enumeratorManager.service(_manager);
336        
337        _customFieldManager = new ThreadSafeComponentManager<>();
338        _customFieldManager.setLogger(getLogger());
339        _customFieldManager.contextualize(_context);
340        _customFieldManager.service(_manager);
341        
342        _customMetadataIndexingFieldManager = new ThreadSafeComponentManager<>();
343        _customMetadataIndexingFieldManager.setLogger(getLogger());
344        _customMetadataIndexingFieldManager.contextualize(_context);
345        _customMetadataIndexingFieldManager.service(_manager);
346        
347        Configuration rootConfiguration = getRootConfiguration(configuration);
348        
349        _abstract = rootConfiguration.getAttributeAsBoolean("abstract", false);
350        
351        _configureSuperTypes(rootConfiguration);
352        
353        _configureLabels(rootConfiguration);
354        _configureIcons(rootConfiguration);
355        
356        _configureCSSFiles(rootConfiguration);
357        
358        // Tags
359        _tags = new HashSet<>();
360        _inheritableTags = new HashSet<>();
361        
362        if (rootConfiguration.getChild("tags", false) != null)
363        {
364            if (rootConfiguration.getChild("tags").getAttributeAsBoolean("inherited", false))
365            {
366                // Get tags from super types
367                for (String superTypeId : _superTypeIds)
368                {
369                    ContentType superType = _cTypeEP.getExtension(superTypeId);
370                    _tags.addAll(superType.getInheritableTags());
371                    _inheritableTags.addAll(superType.getInheritableTags());
372                }
373            }
374            
375            _configureLocalTags(rootConfiguration.getChild("tags"));
376        }
377        
378        // Rights
379        _right = rootConfiguration.getChild("right").getValue(null);
380        
381        _isSimple = true;
382        for (String superTypeId : _superTypeIds)
383        {
384            ContentType superType = _cTypeEP.getExtension(superTypeId);
385            if (superType == null)
386            {
387                throw new ConfigurationException("The content type '" + this.getId() + "' cannot extends the unexisting type '" + superTypeId + "'");
388            }
389            if (!superType.isSimple())
390            {
391                _isSimple = false;
392                break;
393            }
394        }
395        
396        _configureDefaultWorkflowName(rootConfiguration);
397        
398        _isMultilingual = false;
399        for (String superTypeId : _superTypeIds)
400        {
401            ContentType superType = _cTypeEP.getExtension(superTypeId);
402            if (superType.isMultilingual())
403            {
404                _isMultilingual = true;
405                break;
406            }
407        }
408        
409        // Attribute definitions
410        _configureAttributeDefinitions (rootConfiguration);
411        
412        // Parent content type
413        _configureParentContentType(rootConfiguration);
414        
415        // Views
416        _configureViews(rootConfiguration);
417        _configureMetadataSets (rootConfiguration);
418        
419        _checkForReservedAttributeName();
420        
421        if (!_abstract && !hasTag(TAG_MIXIN))
422        {
423            if (!_views.containsKey("details"))
424            {
425                throw new ConfigurationException("Mandatory view named 'details' is missing for content type " + _id, configuration);
426            }
427            if (!_views.containsKey("main"))
428            {
429                throw new ConfigurationException("Mandatory view named 'main' is missing for content type " + _id, configuration);
430            }
431        }
432        
433        // Global validators
434        _configureGlobalValidators (rootConfiguration);
435        
436        // Indexing model
437        _configureIndexingModel (rootConfiguration);
438    }
439    
440    private void _checkForReservedAttributeName() throws ConfigurationException
441    {
442        Set<String> reservedNames = _getContentTypeReservedAttributeNameExtensionPoint().getExtensionsIds();
443        
444        List<String> usedReservedKeywords = new ArrayList<>(reservedNames);
445        usedReservedKeywords.retainAll(_modelItems.keySet());
446        
447        if (!usedReservedKeywords.isEmpty())
448        {
449            throw new ConfigurationException("In content type '" + _id + "', one or more attributes are named with a reserved keyword: " + StringUtils.join(usedReservedKeywords, ", ") 
450            + ". The reserved keywords are: {" + StringUtils.join(reservedNames, ", ") + "}");
451        }
452    }
453
454    /**
455     * Configure attribute definitions
456     * @param mainConfig The content type configuration
457     * @throws ConfigurationException if an error occurred
458     */
459    protected void _configureAttributeDefinitions (Configuration mainConfig) throws ConfigurationException
460    {
461        _attributeDefinitionParser = new ContentAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager);
462        _compositeDefinitionParser = new ContentRestrictedCompositeDefinitionParser(_contentAttributeTypeExtensionPoint);
463        _repeaterDefinitionParser = new ContentRestrictedRepeaterDefinitionParser(_contentAttributeTypeExtensionPoint);
464        _dublinCoreAttributeDefinitionParser = new DublinCoreAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager, _dcProvider);
465        MetadataAndRepeaterDefinitionParser defParser = new MetadataAndRepeaterDefinitionParser(_oldEnumeratorManager, _oldValidatorManager);
466        
467        // First, get metadata from super type if applicable.
468        _modelItems.putAll(_contentTypesHelper.getModelItems(_superTypeIds));
469        _metadata.putAll(_getMetadataDefinitions(_superTypeIds));
470        
471        Map<String, Configuration> attributeConfiguration = new LinkedHashMap<>();
472        _getApplicableAttributes(mainConfig, attributeConfiguration, false);
473        
474        Configuration overriddenConfig = getOverridenConfiguration();
475        if (overriddenConfig != null)
476        {
477            _getApplicableAttributes(overriddenConfig, attributeConfiguration, true);
478        }
479        
480        // Then, parse own attributes
481        _parseAllMetadatas(attributeConfiguration, defParser);
482        _parseAllAttributes(attributeConfiguration);
483        
484        try
485        {
486            _attributeDefinitionParser.lookupComponents();
487//            _dublinCoreAttributeDefinitionParser.lookupComponents();
488            defParser.lookupComponents();
489        }
490        catch (Exception e)
491        {
492            throw new ConfigurationException("Unable to lookup parameter local components", overriddenConfig, e);
493        }
494        
495    }
496    
497    @Deprecated
498    private Map<String, MetadataDefinition> _getMetadataDefinitions(String[] cTypes) throws ConfigurationException
499    {
500        Map<String, MetadataDefinition> metadata = new LinkedHashMap<>();
501
502        for (String id : cTypes)
503        {
504            ContentType cType = _cTypeEP.getExtension(id);
505
506            for (String name : cType.getMetadataNames())
507            {
508                MetadataDefinition definition = cType.getMetadataDefinition(name);
509
510                if (metadata.containsKey(name))
511                {
512                    if (!definition.getReferenceContentType().equals(metadata.get(name).getReferenceContentType()))
513                    {
514                        // The definition does not provide from a common ancestor
515                        throw new ConfigurationException("The metadata '" + name + "' defined in content-type '" + id + "' is already defined in another co-super-type '"
516                                + metadata.get(name).getReferenceContentType() + "'");
517                    }
518                    continue;
519                }
520
521                metadata.put(name, definition);
522            }
523        }
524
525        return metadata;
526    }
527    
528    /**
529     * Fill a map of the applicable attribute configurations.
530     * @param config the content type configuration.
531     * @param attributeConfigurations the Map of attributes {@link Configuration}, indexed by name.
532     * @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. 
533     * @throws ConfigurationException if an error occurs.
534     */
535    protected void _getApplicableAttributes(Configuration config, Map<String, Configuration> attributeConfigurations, boolean allowOverride) throws ConfigurationException
536    {
537        for (Configuration childConfiguration : config.getChildren())
538        {
539            String childName = childConfiguration.getName();
540            
541            if (childName.equals("metadata") || childName.equals("repeater"))
542            {
543                String attributeName = childConfiguration.getAttribute("name", "");
544                
545                if (!allowOverride && attributeConfigurations.containsKey(attributeName))
546                {
547                    throw new ConfigurationException("Attribute with name '" + attributeName + "' is already defined", childConfiguration);
548                }
549                else if (allowOverride && attributeConfigurations.containsKey(attributeName))
550                {
551                    _checkAttributeTypes(attributeConfigurations.get(attributeName), childConfiguration);
552                }
553                
554                if (allowOverride)
555                {
556                    this._overriddenAttributes.add(attributeName);
557                }
558                
559                attributeConfigurations.put(attributeName, childConfiguration);
560            }
561            else if (childName.equals("dublin-core"))
562            {
563                attributeConfigurations.put("dc", childConfiguration);
564            }
565        }
566    }
567    /**
568     * Get the overridden attributes list
569     *  @return the overridden attributes list
570     */
571    public List<String> getOverriddenAttributes()
572    {
573        return this._overriddenAttributes;
574    }
575    
576    /**
577     * Check if all metadata's types defined in first configuration are equals to those defined in second configuration
578     * @param metadataConf1 The first configuration to compare
579     * @param metadataConf2 The second configuration to compare
580     * @throws ConfigurationException if the types are not equals
581     */
582    protected void _checkAttributeTypes (Configuration metadataConf1, Configuration metadataConf2) throws ConfigurationException
583    {
584        String type = metadataConf1.getAttribute("type", "");
585        String overridenType = metadataConf2.getAttribute("type", "");
586        if (!overridenType.equals(type))
587        {
588            throw new ConfigurationException("The type of metadata '" + metadataConf1.getAttribute("name") + " (" + type.toUpperCase() + ")" + "' can not be overriden to '" + metadataConf2.getAttribute("name") + " (" + overridenType.toUpperCase() + ")'");
589        }
590        
591        if ("composite".equals(type) || metadataConf1.getName().equals("repeater"))
592        {
593            for (Configuration childConfig1 : metadataConf1.getChildren())
594            {
595                String childName = childConfig1.getName();
596                if (childName.equals("metadata") || childName.equals("repeater"))
597                {
598                    Configuration childConfig2 = null;
599                    for (Configuration conf : metadataConf2.getChildren(childName))
600                    {
601                        if (childConfig1.getAttribute("name").equals(conf.getAttribute("name")))
602                        {
603                            childConfig2 = conf;
604                            break;
605                        }
606                    }
607                    
608                    if (childConfig2 != null)
609                    {
610                        _checkAttributeTypes (childConfig1, childConfig2);
611                    }
612                }
613            }
614        }
615    }
616    
617    /**
618     * Parse all attribute configurations.
619     * @param attributeConfigurations the attribute configurations.
620     * @throws ConfigurationException if the configuration is invalid.
621     */
622    protected void _parseAllAttributes(Map<String, Configuration> attributeConfigurations) throws ConfigurationException
623    {
624        for (Configuration childConfiguration : attributeConfigurations.values())
625        {
626            String childConfigName = childConfiguration.getName();
627            
628            if (childConfigName.equals("metadata") || childConfigName.equals("repeater"))
629            {
630                ModelItem child = _parseModelItem(childConfiguration, null);
631                if (child != null)
632                {
633                    final String childName = child.getName();
634                    if (_modelItems.containsKey(childName))
635                    {
636                        _checkAttributeTypes(_modelItems.get(childName), child);
637                    }
638                    
639                    _checkContentTypeSimplicity(child);
640                    _modelItems.put(childName, child);
641                }
642            }
643            else if (childConfigName.equals("dublin-core"))
644            {
645                _parseDublinCoreAttributes();
646            }
647        }
648    }
649    
650    /**
651     * Parses a model item
652     * @param itemConfiguration configuration of the model item to parse
653     * @param parent the parent of the model item to parse. Can be <code>null</code> if the item has no parent.
654     * @return the parsed model item
655     * @throws ConfigurationException if an error occurs while the model item is parsed
656     */
657    @SuppressWarnings("static-access")
658    protected ModelItem _parseModelItem(Configuration itemConfiguration, ModelItemGroup parent) throws ConfigurationException
659    {
660        ModelItem modelItem = null;
661        final String itemConfigName = itemConfiguration.getName();
662        if (itemConfigName.equals("metadata"))
663        {
664            String typeId = itemConfiguration.getAttribute("type");
665            if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(typeId))
666            {
667                modelItem = _compositeDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent);
668            }
669            else
670            {
671                modelItem = _attributeDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent);
672            }
673        }
674        else if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(itemConfigName))
675        {
676            modelItem = _repeaterDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent);
677        }
678        
679        if (modelItem != null && modelItem instanceof ModelItemGroup)
680        {
681            for (Configuration childConfiguration : itemConfiguration.getChildren())
682            {
683                _parseModelItem(childConfiguration, (ModelItemGroup) modelItem);
684            }
685        }
686        
687        return modelItem;
688    }
689    
690    /**
691     * Check if all attribute types defined in first model item are equals to those defined in second model item
692     * @param item1 The first item to compare
693     * @param item2 The second item to compare
694     * @throws ConfigurationException if the types are not equals
695     */
696    protected void _checkAttributeTypes (ModelItem item1, ModelItem item2) throws ConfigurationException
697    {
698        if (item1 instanceof AttributeDefinition)
699        {
700            AttributeDefinition attributeDef1 = (AttributeDefinition) item1;
701            final String attibute1TypeId = attributeDef1.getType().getId();
702            if (!(item2 instanceof AttributeDefinition) || !attibute1TypeId.equals(((AttributeDefinition) item2).getType().getId()))
703            {
704                throw new ConfigurationException("The type of attribute '" + attributeDef1 + "' defined in content type '" + attributeDef1.getModel() + "', can not be overriden to '" + item2 + "' in content type '" + ((AttributeDefinition) item2).getModel() + "'");
705            }
706        }
707        else
708        {
709            if (!(item2 instanceof ModelItemGroup))
710            {
711                throw new ConfigurationException("The item group '" + item1 + "' can not be overriden by the non item group '" + item2 + "' in content type '" + ((AttributeDefinition) item2).getModel() + "'");
712            }
713
714            ModelItemGroup group1 = (ModelItemGroup) item1;
715            ModelItemGroup group2 = (ModelItemGroup) item2;
716            
717            for (ModelItem subItemfromGroup1 : group1.getChildren())
718            {
719                ModelItem subItemFromGroup2 = group2.getChild(subItemfromGroup1.getName());
720                if (subItemFromGroup2 != null)
721                {
722                    _checkAttributeTypes(subItemfromGroup1, subItemFromGroup2);
723                    _checkContentTypeSimplicity(subItemfromGroup1);
724                }
725            }
726        }
727    }
728    
729    /**
730     * Checks the given model item to determine if this content type is multilingual and/or simple
731     * All items of a simple content-type have to be a simple type (string, long, date, ..)
732     * A multilingual content type should contain at least an attribute of type MULTILINGUAL-STRING
733     * @param modelItem The model item to check
734     */
735    protected void _checkContentTypeSimplicity(ModelItem modelItem)
736    {
737        if (modelItem instanceof ModelItemGroup)
738        {
739            // If the content type contains groups, it is not simple
740            _isSimple = false;
741        }
742        else if (modelItem instanceof AttributeDefinition)
743        {
744            ElementType type = ((AttributeDefinition) modelItem).getType();
745            if (!type.isSimple())
746            {
747                // If there is a no simple attribute, the content type is not simple 
748                _isSimple = false;
749            }
750            
751            if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(type.getId()))
752            {
753                // If there is a multilingual-string attribute, the content type is multilingual
754                _isMultilingual = true;
755            }
756        }
757    }
758    
759    /**
760     * Parse DublinCore attributes
761     * @throws ConfigurationException if the configuration is invalid
762     */
763    @SuppressWarnings("static-access")
764    protected void _parseDublinCoreAttributes() throws ConfigurationException
765    {
766        Source src = null;
767        
768        try
769        {
770            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
771            
772            if (src.exists())
773            {
774                Configuration configuration = null;
775                try (InputStream is = src.getInputStream())
776                {
777                    configuration = new DefaultConfigurationBuilder(true).build(is);
778                }
779                
780                ContentRestrictedCompositeDefinition definition = new ContentRestrictedCompositeDefinition();
781                definition.setModel(this);
782                definition.setName("dc");
783                definition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL"));
784                definition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC"));
785                definition.setType(_contentAttributeTypeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID));
786                
787                for (Configuration childConfiguration : configuration.getChildren())
788                {
789                    String childName = childConfiguration.getName();
790                    
791                    if (childName.equals("metadata"))
792                    {
793                        _dublinCoreAttributeDefinitionParser.parse(_manager, _pluginName, childConfiguration, this, definition);
794                    }
795                }
796                
797                _modelItems.put("dc", definition);
798            }
799        }
800        catch (IOException | SAXException e)
801        {
802            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
803        }
804        finally
805        {
806            if (src != null)
807            {
808                _srcResolver.release(src);
809            }
810        }
811    }
812    
813    /**
814     * Parse all metadata configurations.
815     * @param metadataConfigurations the metadata configurations.
816     * @param defParser the metadata definition parser.
817     * @throws ConfigurationException if the configuration is invalid.
818     * @deprecated use {@link #_parseAllAttributes(Map)} instead
819     */
820    @Deprecated
821    protected void _parseAllMetadatas(Map<String, Configuration> metadataConfigurations, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
822    {
823        for (Configuration childConfiguration : metadataConfigurations.values())
824        {
825            String childName = childConfiguration.getName();
826            
827            if (childName.equals("metadata") || childName.equals("repeater"))
828            {
829                _parseMetadata(childConfiguration, defParser);
830            }
831            else if (childName.equals("dublin-core"))
832            {
833                _parseDublinCoreMetadata(defParser);
834            }
835        }
836    }
837    
838    /**
839     * Parse a metadata configuration.
840     * @param metadataConfiguration the metadata configuration.
841     * @param defParser the metadata definition parser.
842     * @return the created MetadataDefinition.
843     * @throws ConfigurationException if the configuration is invalid
844     * @deprecated use {@link #_parseModelItem(Configuration, ModelItemGroup)} instead
845     */
846    @Deprecated
847    protected MetadataDefinition _parseMetadata(Configuration metadataConfiguration, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
848    {
849        MetadataDefinition metadataDefinition = defParser.parseParameter(_manager, _pluginName, metadataConfiguration);
850        metadataDefinition.setReferenceContentType(_id);
851        
852        String metadataName = metadataDefinition.getName();
853        
854        if (_metadata.containsKey(metadataName))
855        {
856            _checkMetadataTypes (_metadata.get(metadataName), metadataDefinition);
857        }
858        
859        // Update simple and multilingual properties
860        _checkMetadataDefinition(metadataDefinition);
861        
862        _metadata.put(metadataName, metadataDefinition);
863        
864        return metadataDefinition;
865    }
866    
867    /**
868     * Check if all metadata's types defined in first metadata definition are equals to those defined in second metadata definition
869     * @param metaDef1 The first metadata definition to compare
870     * @param metaDef2 The second metadata definition to compare
871     * @throws ConfigurationException if the types are not equals
872     * @deprecated Use {@link #_checkAttributeTypes(ModelItem, ModelItem)} instead
873     */
874    @Deprecated
875    protected void _checkMetadataTypes (MetadataDefinition metaDef1, MetadataDefinition metaDef2) throws ConfigurationException
876    {
877        if (!metaDef1.getType().equals(metaDef2.getType()))
878        {
879            throw new ConfigurationException("The type of metadata '" + metaDef1.toString() + "' defined in content type '" + metaDef1.getReferenceContentType() + "', can not be overriden to '" + metaDef2.toString() + "' in content type '" + metaDef2.getReferenceContentType() + "'");
880        }
881        
882        if (metaDef1.getType().equals(MetadataType.COMPOSITE))
883        {
884            for (String subMetadataName : metaDef1.getMetadataNames())
885            {
886                if (metaDef2.getMetadataDefinition(subMetadataName) != null)
887                {
888                    _checkMetadataTypes (metaDef1.getMetadataDefinition(subMetadataName), metaDef2.getMetadataDefinition(subMetadataName));
889                 
890                    // Update simple and multilingual properties
891                    _checkMetadataDefinition(metaDef1.getMetadataDefinition(subMetadataName));
892                }
893            }
894        }
895    }
896    
897    /**
898     * Check the medatata definition to determines if this content type is multilingual and/or simple
899     * All medatata of a simple content-type have to be a simple type (string, long, date, ..)
900     * A multilingual content type should contain at least a metadata of type MULTILINGUAL_STRING
901     * @param metadataDefinition The metadata definition
902     * @return false if the medatata definition is not a valid medatata definition for a simple content-type
903     * @deprecated Use {@link #_checkContentTypeSimplicity(ModelItem)} instead
904     */
905    @Deprecated
906    protected boolean _checkMetadataDefinition (MetadataDefinition metadataDefinition)
907    {
908        MetadataType type = metadataDefinition.getType();
909        
910        switch (type)
911        {
912            case MULTILINGUAL_STRING:
913                // The content type is a multilingual content type
914                _isMultilingual = true;
915                break;
916                
917            case COMPOSITE:
918            case FILE:
919            case REFERENCE:
920            case RICH_TEXT:
921                // The content type can not be simple (complex metadata are not allowed)
922                _isSimple = false;
923                break;
924            default:
925                break;
926        }
927        
928        return true;
929    }
930    
931    /**
932     * Parse DublinCore metadata
933     * @param defParser The parser definition
934     * @throws ConfigurationException if the configuration is invalid
935     * @deprecated Use {@link #_parseDublinCoreAttributes()} instead
936     */
937    @Deprecated
938    protected void _parseDublinCoreMetadata (MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
939    {
940        Source src = null;
941        
942        try
943        {
944            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
945            
946            if (src.exists())
947            {
948                Configuration configuration = null;
949                try (InputStream is = src.getInputStream())
950                {
951                    configuration = new DefaultConfigurationBuilder(true).build(is);
952                }
953                
954                MetadataDefinition metadataDefinition = new MetadataDefinition();
955                metadataDefinition.setReferenceContentType(_id);
956                metadataDefinition.setId("/dc"); // FIXME ?
957                metadataDefinition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL"));
958                metadataDefinition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC"));
959                metadataDefinition.setName("dc");
960                metadataDefinition.setType(MetadataType.COMPOSITE);
961                
962                for (Configuration childConfiguration : configuration.getChildren())
963                {
964                    String childName = childConfiguration.getName();
965                    
966                    if (childName.equals("metadata"))
967                    {
968                        MetadataDefinition metaDef = defParser.parseParameter(_manager, _pluginName, childConfiguration);
969                        metaDef.setReferenceContentType(_id);
970                        String metadataName = metaDef.getName();
971                        metaDef.setId("/dc/" + metadataName);  // FIXME ?
972                        
973                        if (metaDef.getEnumerator() == null && _dcProvider.isEnumerated(metadataName))
974                        {
975                            StaticEnumerator enumerator = new StaticEnumerator();
976                            
977                            Map<String, I18nizableText> entries = _dcProvider.getEntries(metadataName);
978                            if (entries != null)
979                            {
980                                for (String value : entries.keySet())
981                                {
982                                    enumerator.add(entries.get(value), value);
983                                }
984                               
985                            }
986                            metaDef.setEnumerator(enumerator);
987                        }
988                        
989                        metadataDefinition.addMetadata(metaDef);
990                    }
991                }
992                
993                _metadata.put("dc", metadataDefinition);
994            }
995        }
996        catch (IOException e)
997        {
998            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
999        }
1000        catch (SAXException e)
1001        {
1002            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
1003        }
1004        finally
1005        {
1006            if (src != null)
1007            {
1008                _srcResolver.release(src);
1009            }
1010        }
1011    }
1012    
1013    /**
1014     * Configure the default workflow name from the XML configuration.
1015     *  - From the overriden configuration
1016     *  - If not, from the current configuration
1017     *  - If not, from the supertypes
1018     *  - If it cannot be determined and the content type is a reference table, then "reference-table"
1019     *  - Otherwise "content"
1020     * @param mainConfig The configuration
1021     * @throws ConfigurationException if an exception occurs
1022     */
1023    protected void _configureDefaultWorkflowName(Configuration mainConfig) throws ConfigurationException
1024    {
1025        _configuredWorkflowNames = Optional.ofNullable(getOverridenConfiguration())
1026            // Override mode
1027            .map(oc -> oc.getChild("default-workflow").getValue(null))
1028            // Normal mode
1029            .or(() -> Optional.ofNullable(mainConfig.getChild("default-workflow").getValue(null)))
1030            // Transform it in a singleton set
1031            .map(Set::of)
1032            // Inherited mode
1033            .orElseGet(this::_getDefaultWorkflowNamesFromSupertypes);
1034        
1035        _defaultWorkflowName = _configuredWorkflowNames.size() > 1
1036                // Several workflow names returns undefined
1037                ? Optional.empty()
1038                : _configuredWorkflowNames.stream()
1039                    // One workflow name is directly returned
1040                    .findFirst()
1041                    // Otherwise default workflow : reference-table or content
1042                    .or(() -> Optional.of(isReferenceTable() ? "reference-table" : "content"));
1043    }
1044    
1045    private Set<String> _getDefaultWorkflowNamesFromSupertypes()
1046    {
1047        Set<String> defaultWorkflowNames = new HashSet<>();
1048        
1049        // Get tags from super types
1050        for (String superTypeId : _superTypeIds)
1051        {
1052            ContentType superType = _cTypeEP.getExtension(superTypeId);
1053            defaultWorkflowNames.addAll(superType.getConfiguredDefaultWorkflowNames());
1054        }
1055        
1056        if (defaultWorkflowNames.size() > 1)
1057        {
1058            getLogger().warn("Several default workflows are defined for content type '{}' : {}.", _id, StringUtils.join(defaultWorkflowNames));
1059        }
1060        
1061        return defaultWorkflowNames;
1062    }
1063
1064    /**
1065     * Configures the "parent" content type. 
1066     * This must not be confounded with the super types. (See {@link AbstractContentTypeDescriptor#_configureSuperTypes(Configuration)})
1067     * @param mainConfig The main configuration
1068     * @throws ConfigurationException if an error occurred
1069     */
1070    protected void _configureParentContentType(Configuration mainConfig) throws ConfigurationException
1071    {
1072        Configuration parentCTypeConf = mainConfig.getChild("parent-ref", false);
1073        _parentAttributeDefinition = null;
1074        
1075        if (parentCTypeConf != null)
1076        {
1077            // Check this content type is a reference table
1078            if (!isReferenceTable())
1079            {
1080                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());
1081                return;
1082            }
1083            
1084            String refAttributeName = parentCTypeConf.getAttribute("name");
1085            // Check valid reference of metadata
1086            if (!_modelItems.containsKey(refAttributeName))
1087            {
1088                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it does not exist. It will be ignored.", getId(), refAttributeName);
1089                return;
1090            }
1091            
1092            ModelItem modelItem = _modelItems.get(refAttributeName);
1093            // Check metadata of type "content"
1094            if (!(modelItem instanceof ContentAttributeDefinition))
1095            {
1096                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);
1097                return;
1098            }
1099            
1100            ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) modelItem;
1101            if (contentAttributeDefinition.isMultiple())
1102            {
1103                getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it is a multiple attribute. It will be ignored.", getId(), refAttributeName);
1104                return;
1105            }
1106            
1107            String parentCTypeName = contentAttributeDefinition.getContentTypeId();
1108            ContentType parentCType = _cTypeEP.getExtension(parentCTypeName);
1109            
1110            if (parentCType == null)
1111            {
1112                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);
1113                return;
1114            }
1115            // Check parent content type is private AND simple
1116            else if (!parentCType.isPrivate())
1117            {
1118                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);
1119                return;
1120            }
1121            else if (!parentCType.isReferenceTable())
1122            {
1123                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);
1124                return;
1125            }
1126            
1127            if (!_hierarchicalSimpleContentsHelper.registerRelation(parentCType, this))
1128            {
1129                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());
1130                return;
1131            }
1132            _parentAttributeDefinition = contentAttributeDefinition;
1133        }
1134    }
1135    
1136    /**
1137     * Configure the content type views
1138     * @param mainConfig The content type configuration
1139     * @throws ConfigurationException if an error occurred
1140     */
1141    protected void _configureViews(Configuration mainConfig) throws ConfigurationException
1142    {
1143        // Get applicable views configurations
1144        // At first, get the views configured with the old syntax to replace them by the ones configured with the new syntax
1145        _viewConfigurations = _getApplicableViews(mainConfig, VIEW_TAG_NAME_WITH_LEGACY_SYNTAX, false);
1146        _viewConfigurations.putAll(_getApplicableViews(mainConfig, VIEW_TAG_NAME, false));
1147        
1148        Configuration overriddenConfig = getOverridenConfiguration();
1149        if (overriddenConfig != null)
1150        {
1151            _viewConfigurations.putAll(_getApplicableViews(overriddenConfig, VIEW_TAG_NAME_WITH_LEGACY_SYNTAX, true));
1152            _viewConfigurations.putAll(_getApplicableViews(overriddenConfig, VIEW_TAG_NAME, true));
1153        }
1154        
1155        Map<String, Map<ContentType, Configuration>> allApplicableViews = _contentTypesHelper.getViewConfigurations(_superTypeIds, new String[0]);
1156        for (Map.Entry<String, Configuration> viewConfigurationByViewName : _viewConfigurations.entrySet())
1157        {
1158            String viewName = viewConfigurationByViewName.getKey();
1159            Configuration viewConfiguration = viewConfigurationByViewName.getValue();
1160            allApplicableViews.put(viewName, Map.of(this, viewConfiguration));
1161        }
1162        
1163        // Parse content type's views
1164        for (String viewName : allApplicableViews.keySet())
1165        {
1166            View view = _contentTypesHelper.parseView(this, allApplicableViews.get(viewName));
1167            _views.put(viewName, view);
1168        }
1169    }
1170    
1171    /**
1172     * Get the overridden views list
1173     *  @return the overridden views list
1174     */
1175    public List<String> getOverriddenViews()
1176    {
1177        return this._overriddenViews;
1178    }
1179    
1180    /**
1181     * Compute the applicable views from their configurations.
1182     * @param configuration The content type configuration
1183     * @param viewTagName The name of the tag containing the view
1184     * @param allowOverride if <code>true</code>, encountering a view which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown.
1185     * @return the applicable views, indexed by their names. For each view, indicates if the view is configured 
1186     * @throws ConfigurationException if the configuration is invalid
1187     */
1188    protected Map<String, Configuration> _getApplicableViews(Configuration configuration, String viewTagName, boolean allowOverride) throws ConfigurationException
1189    {
1190        Map<String, Configuration> applicableViews = new LinkedHashMap<>();
1191        
1192        for (Configuration viewConfig : configuration.getChildren(viewTagName))
1193        {
1194            String name = viewConfig.getAttribute("name");
1195            if (allowOverride)
1196            {
1197                this._overriddenViews.add(name);
1198            }
1199            
1200            if (!allowOverride && applicableViews.containsKey(name))
1201            {
1202                // TODO NEWATTRIBUTEAPI_CONTENT: Review the unicity of views due to edition / view differences :
1203                // In old API, we made the difference between view and edition metadata set.
1204                // We decided remove this notion. But all existing content types have for example 2 "main" metadata-set, one for view and the other for edition
1205                // So we can't check here that there is onl one view called "main"
1206                // Really remove the edition / view notion? or allow the creation of a view without type and put it in edition and view mode?
1207//                throw new ConfigurationException("The view named '" + name + "' is already defined.", viewConfig);
1208                getLogger().info("The view named '" + name + "' is already defined.", viewConfig);
1209            }
1210            else
1211            {
1212                applicableViews.put(name, viewConfig);
1213            }
1214        }
1215        
1216        return applicableViews;
1217    }
1218    
1219    /**
1220     * Configure the global validators for content type
1221     * @param config The content type configuration
1222     * @throws ConfigurationException if an error occurs
1223     */
1224    protected void _configureGlobalValidators (Configuration config) throws ConfigurationException
1225    {
1226        _globalValidators = new ArrayList<>();
1227        List<String> globalValidatorsToLookup = new ArrayList<>();
1228        
1229        Configuration globalValidatorsConfig = config.getChild("global-validators", true);
1230        globalValidatorsToLookup.addAll(_parseGlobalValidators(globalValidatorsConfig, config.getAttributeAsBoolean("include-from-supertype", true)));
1231        
1232        Configuration overriddenConfig = getOverridenConfiguration();
1233        if (overriddenConfig != null)
1234        {
1235            // Global validators into an overriden configuration are added to the original global validators
1236            globalValidatorsToLookup.addAll(_parseGlobalValidators(overriddenConfig.getChild("global-validators", true), false));
1237        }
1238        
1239        try
1240        {
1241            _globalValidatorsManager.initialize();
1242        }
1243        catch (Exception e)
1244        {
1245            throw new ConfigurationException("Unable to initialize global validator manager", e);
1246        }
1247        
1248        for (String validatorRole : globalValidatorsToLookup)
1249        {
1250            try
1251            {
1252                ContentValidator contentValidator = _globalValidatorsManager.lookup(validatorRole);
1253                contentValidator.setContentType(this);
1254                
1255                _globalValidators.add(contentValidator);
1256            }
1257            catch (ComponentException e)
1258            {
1259                throw new ConfigurationException("Unable to lookup global validator role: '" + validatorRole + "' for content type: " + this.getId(), e);
1260            }
1261        }
1262        
1263    }
1264    
1265    /**
1266     * Parse the global validators
1267     * @param config the configuration
1268     * @param includeSuperTypeValidators true to include validators of super types
1269     * @return the role of global validators to be lookuped
1270     * @throws ConfigurationException if configuration is incorrect
1271     */
1272    @SuppressWarnings("unchecked")
1273    protected List<String> _parseGlobalValidators(Configuration config, boolean includeSuperTypeValidators) throws ConfigurationException
1274    {
1275        List<String> gvRoles = new ArrayList<>();
1276        
1277        if (includeSuperTypeValidators)
1278        {
1279            for (String superTypeId : _superTypeIds)
1280            {
1281                ContentType cType = _cTypeEP.getExtension(superTypeId);
1282                _globalValidators.addAll(cType.getGlobalValidators());
1283            }
1284        }
1285        
1286        for (Configuration globalValidatorConfig : config.getChildren("global-validator"))
1287        {
1288            String globalValidatorId = __GLOBAL_VALIDATOR_ROLE_PREFIX + RandomStringUtils.randomAlphanumeric(10);
1289            String validatorClassName = globalValidatorConfig.getAttribute("class");
1290            
1291            try
1292            {
1293                Class validatorClass = Class.forName(validatorClassName);
1294                _globalValidatorsManager.addComponent(_pluginName, null, globalValidatorId, validatorClass, globalValidatorConfig);
1295            }
1296            catch (Exception e)
1297            {
1298                throw new ConfigurationException("Unable to instantiate global validator for class: " + validatorClassName, e);
1299            }
1300            
1301            gvRoles.add(globalValidatorId);
1302        }
1303        
1304        return gvRoles;
1305    }
1306    
1307    /**
1308     * Configure the indexing model
1309     * @param config The main configuration
1310     * @throws ConfigurationException if an error occurred
1311     */
1312    protected void _configureIndexingModel (Configuration config) throws ConfigurationException
1313    {
1314        Configuration indexConf = config.getChild("indexing-model", true);
1315        
1316        Configuration overriddenConfig = getOverridenConfiguration();
1317        if (overriddenConfig != null && overriddenConfig.getChild("indexing-model", false) != null)
1318        {
1319            // Indexing model is overriden (original configuration is ignored)
1320            indexConf = overriddenConfig.getChild("indexing-model", true);
1321        }
1322        
1323        _indexingModel = new IndexingModel();
1324        
1325        boolean includeFromSuperType = indexConf.getAttributeAsBoolean("include-from-supertype", true);
1326        if (includeFromSuperType)
1327        {
1328            _indexingModel = _contentTypesHelper.getIndexingModel(_superTypeIds, new String[0]);
1329        }
1330        
1331        boolean includeAll = indexConf.getAttributeAsBoolean("include-all", true);
1332        if (includeAll)
1333        {
1334            for (String metadataName : _metadata.keySet())
1335            {
1336                MetadataDefinition definition = _metadata.get(metadataName);
1337                _indexingModel.addIndexingField(new DefaultMetadataIndexingField(metadataName, definition, metadataName));
1338            }
1339        }
1340        
1341        // Optionally add the semantic annotations to the indexing model.
1342        boolean includeSemanticAnnotations = indexConf.getAttributeAsBoolean("include-semantic-annotations", true);
1343        if (includeSemanticAnnotations)
1344        {
1345            _addSemanticAnnotations(indexConf, this);
1346        }
1347        
1348        // Metadata fields.
1349        _configureMetadataIndexingFields(indexConf);
1350        
1351        // Custom fields.
1352        _configureCustomIndexingFields(indexConf);
1353        
1354        // Custom metadata fields.
1355        _configureCustomMetadataIndexingFields(indexConf);
1356    }
1357    
1358    /**
1359     * Add semantic annotations as indexing fields to the indexing model.
1360     * @param indexConf the indexing model configuration.
1361     * @param holder the metadata holder (ContentType or MetadataDefinition) to scan for annotable metadata.
1362     */
1363    protected void _addSemanticAnnotations(Configuration indexConf, MetadataDefinitionHolder holder)
1364    {
1365        // Get the semantic annotations in a multimap.
1366        Multimap<SemanticAnnotation, String> metadatas = HashMultimap.create();
1367        _getSemanticAnnotations(holder, metadatas, "");
1368        
1369        // Add a custom indexing field for each annotation to the indexing model.
1370        for (SemanticAnnotation annotation : metadatas.keySet())
1371        {
1372            Collection<String> metaPaths = metadatas.get(annotation);
1373            _indexingModel.addIndexingField(new SemanticAnnotationIndexingField(annotation, metaPaths, this));
1374        }
1375    }
1376    
1377    /**
1378     * Get the semantic annotations and their paths from the given metadata definition holder's sub-tree.
1379     * @param holder The current metadata definition holder.
1380     * @param annotations The map of semantic annotations and the corresponding metadata paths.
1381     * @param prefix The current prefix in the metadata tree (with a trailing slash, if applicable).
1382     */
1383    protected void _getSemanticAnnotations(MetadataDefinitionHolder holder, Multimap<SemanticAnnotation, String> annotations, String prefix)
1384    {
1385        for (String metadataName : holder.getMetadataNames())
1386        {
1387            String metaPath = prefix + metadataName;
1388            MetadataDefinition metaDef = holder.getMetadataDefinition(metadataName);
1389            if (metaDef instanceof AnnotableDefinition)
1390            {
1391                List<SemanticAnnotation> metaAnnotations = ((AnnotableDefinition) metaDef).getSemanticAnnotations();
1392                for (SemanticAnnotation annotation : metaAnnotations)
1393                {
1394                    annotations.put(annotation, metaPath);
1395                }
1396            }
1397            
1398            _getSemanticAnnotations(metaDef, annotations, metaPath + ContentConstants.METADATA_PATH_SEPARATOR);
1399        }
1400    }
1401    
1402    /**
1403     * Configure the metadata indexing fields.
1404     * @param indexConf the indexing model configuration.
1405     * @throws ConfigurationException if an error occurs.
1406     */
1407    protected void _configureMetadataIndexingFields(Configuration indexConf) throws ConfigurationException
1408    {
1409        Configuration[] fieldsConf = indexConf.getChildren("metadata-field");
1410        for (Configuration fieldConf : fieldsConf)
1411        {
1412            String metadataPath = fieldConf.getAttribute("path");
1413            
1414            MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(metadataPath, this);
1415            if (metadataDef != null)
1416            {
1417                String fieldName = fieldConf.getAttribute("name", null);
1418                if (fieldName == null)
1419                {
1420                    fieldName = StringUtils.substringBeforeLast(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1421                }
1422                
1423                // TODO check if metadataDef.getType() is a primitive type (string, long, double, ..) ?? 
1424                /*if (MetadataType.COMPOSITE == metadataDef.getType())
1425                {
1426                    throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path.");
1427                }*/
1428                _indexingModel.addIndexingField(new DefaultMetadataIndexingField(fieldName, metadataDef, metadataPath));
1429            }
1430            else
1431            {
1432                throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path.", fieldConf);
1433            }
1434        }
1435    }
1436
1437    /**
1438     * Configure the custom indexing fields.
1439     * @param indexConf the indexing model configuration.
1440     * @throws ConfigurationException if an error occurs.
1441     */
1442    @SuppressWarnings("unchecked")
1443    protected void _configureCustomIndexingFields(Configuration indexConf) throws ConfigurationException
1444    {
1445        List<String> customFieldRoles = new ArrayList<>();
1446        
1447        Configuration[] customsConf = indexConf.getChildren("custom-field");
1448        for (Configuration customConf : customsConf)
1449        {
1450            String className = customConf.getAttribute("class", null);
1451            if (className == null)
1452            {
1453                throw new ConfigurationException("A custom index field defined in content type '" + this.getId() + "' does not specifiy a class.", customConf);
1454            }
1455            
1456            try
1457            {
1458                Class<CustomIndexingField> customFieldClass = (Class<CustomIndexingField>) Class.forName(className);
1459                String fieldRole = getId() + "-" + className;
1460                _customFieldManager.addComponent("cms", null, getId() + "-" + className, customFieldClass, customConf);
1461                
1462                customFieldRoles.add(fieldRole);
1463            }
1464            catch (Exception e)
1465            {
1466                throw new ConfigurationException("Unable to instanciate custom indexing field for class: " + className, customConf, e);
1467            }
1468        }
1469        
1470        try
1471        {
1472            _customFieldManager.initialize();
1473        }
1474        catch (Exception e)
1475        {
1476            throw new ConfigurationException("Unable to initialize custom indexing field manager", e);
1477        }
1478        
1479        for (String customFieldRole : customFieldRoles)
1480        {
1481            try
1482            {
1483                CustomIndexingField field = _customFieldManager.lookup(customFieldRole);
1484                _indexingModel.addIndexingField(field);
1485            }
1486            catch (ComponentException e)
1487            {
1488                throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customFieldRole + "' for content type: " + this.getId(), e);
1489            }
1490        }
1491    }
1492    
1493    /**
1494     * Configure the custom metadata indexing fields.
1495     * @param indexConf the indexing model configuration.
1496     * @throws ConfigurationException if an error occurs.
1497     */
1498    @SuppressWarnings("unchecked")
1499    protected void _configureCustomMetadataIndexingFields(Configuration indexConf) throws ConfigurationException
1500    {
1501        List<String> customMetadataFieldRoles = new ArrayList<>();
1502        
1503        int index = 0;
1504        Configuration[] customMetaConfs = indexConf.getChildren("custom-metadata-field");
1505        for (Configuration customMetaConf : customMetaConfs)
1506        {
1507            String className = customMetaConf.getAttribute("class", null);
1508            if (className == null)
1509            {
1510                throw new ConfigurationException("A custom indexing field defined in content type '" + this.getId() + "' does not specifiy a class.", customMetaConf);
1511            }
1512            
1513            try
1514            {
1515                DefaultConfiguration localConf = new DefaultConfiguration(customMetaConf, true);
1516                DefaultConfiguration cTypeConf = new DefaultConfiguration("contentType");
1517                cTypeConf.setAttribute("id", this.getId());
1518                localConf.addChild(cTypeConf);
1519                
1520                // Use an index in the role to be able to use several times the same class in a content-type.
1521                String fieldRole = getId() + "-" + className + "-" + index;
1522                Class<CustomMetadataIndexingField> customMetaFieldClass = (Class<CustomMetadataIndexingField>) Class.forName(className);
1523                _customMetadataIndexingFieldManager.addComponent("cms", null, fieldRole, customMetaFieldClass, localConf);
1524                
1525                customMetadataFieldRoles.add(fieldRole);
1526                
1527                index++;
1528            }
1529            catch (Exception e)
1530            {
1531                throw new ConfigurationException("Unable to instanciate custom metadata indexing field for class: " + className, customMetaConf, e);
1532            }
1533        }
1534        
1535        try
1536        {
1537            _customMetadataIndexingFieldManager.initialize();
1538        }
1539        catch (Exception e)
1540        {
1541            throw new ConfigurationException("Unable to initialize custom metadata indexing field manager", e);
1542        }
1543        
1544        for (String customMetaFieldRole : customMetadataFieldRoles)
1545        {
1546            try
1547            {
1548                CustomMetadataIndexingField field = _customMetadataIndexingFieldManager.lookup(customMetaFieldRole);
1549                _indexingModel.addIndexingField(field);
1550            }
1551            catch (ComponentException e)
1552            {
1553                throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customMetaFieldRole + "' for content type: " + this.getId(), e);
1554            }
1555        }
1556    }
1557    
1558    /**
1559     * Parse the tags and add it to tags list
1560     * @param configuration the configuration to use
1561     * @throws ConfigurationException if the configuration is not valid.
1562     */
1563    protected void _configureLocalTags(Configuration configuration)  throws ConfigurationException
1564    {
1565        Configuration[] children = configuration.getChildren("tag");
1566        for (Configuration tagConfig : children)
1567        {
1568            String tagValue = tagConfig.getValue();
1569            if (tagConfig.getAttributeAsBoolean("inheritable", true))
1570            {
1571                _inheritableTags.add(tagValue);
1572            }
1573            _tags.add(tagValue);
1574        }
1575    }
1576    
1577    @Override
1578    public void postInitialize() throws Exception
1579    {
1580        _checkTitleAttribute();
1581        
1582        _checkContentAttributes(this, 0);
1583        
1584        _resolveViewReferences();
1585        
1586        _computeIndexingModelReferences();
1587    }
1588    
1589    @SuppressWarnings("static-access")
1590    private void _checkTitleAttribute() throws ConfigurationException
1591    {
1592        if (!isAbstract() && !isMixin())
1593        {
1594            ModelItem titleItem = null;
1595            if (hasModelItem(Content.ATTRIBUTE_TITLE))
1596            {
1597                titleItem = getModelItem(Content.ATTRIBUTE_TITLE);
1598            }
1599            else
1600            {
1601                // The title attribute is mandatory for non abstract content types
1602                throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined.");
1603            }
1604            
1605            // The title attribute should'nt be a group item
1606            if (!(titleItem instanceof ElementDefinition))
1607            {
1608                throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined.");
1609            }
1610            
1611            // The title attribute should be a string or a multilingual string
1612            ElementDefinition titleAttribute = (ElementDefinition) titleItem;
1613            String typeId = titleAttribute.getType().getId();
1614            if (!ModelItemTypeConstants.STRING_TYPE_ID.equals(typeId) && !ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(typeId))
1615            {
1616                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");
1617            }
1618            
1619            // The title attribute should not be multiple
1620            if (titleAttribute.isMultiple())
1621            {
1622                throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should not be multiple.");
1623            }
1624            
1625            // The title attribute must be mandatory
1626            Validator titleValidator = titleAttribute.getValidator();
1627            if (titleValidator == null || !(boolean) titleValidator.getConfiguration().get("mandatory"))
1628            {
1629                throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should be mandatory.");
1630            }
1631        }
1632    }
1633    
1634    /**
1635     * Check for each content attribute: the content type id, the mutual references and the default values
1636     * @param modelItemContainer the {@link ModelItemContainer} to check
1637     * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater.
1638     * @throws ConfigurationException if a content attribute has an invalid configuration
1639     */
1640    protected void _checkContentAttributes(ModelItemContainer modelItemContainer, int repeaterLevel) throws ConfigurationException
1641    {
1642        for (ModelItem modelItem : modelItemContainer.getModelItems())
1643        {
1644            if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()))
1645            {
1646                ContentAttributeDefinition definition = (ContentAttributeDefinition) modelItem;
1647                _checkContentTypeId(definition);
1648                _checkMutualReferences(definition, repeaterLevel);
1649                definition.checkDefaultValue();
1650            }
1651            
1652            // Check sub-attributes
1653            if (modelItem instanceof ModelItemContainer)
1654            {
1655                // Increment the repeater level if the current attribute is a repeater.
1656                int newRepeaterLevel = (modelItem instanceof org.ametys.plugins.repository.model.RepeaterDefinition) ? repeaterLevel + 1 : repeaterLevel;
1657                _checkContentAttributes((ModelItemContainer) modelItem, newRepeaterLevel);
1658            }
1659        }
1660    }
1661    
1662    /**
1663     * Check the content type id of the given content attribute definition
1664     * @param definition the definition to check
1665     * @throws ConfigurationException if the given content attribute references an invalid or non-existing content type.
1666     */
1667    protected void _checkContentTypeId(ContentAttributeDefinition definition) throws ConfigurationException
1668    {
1669        String contentTypeId = definition.getContentTypeId();
1670        
1671        if (StringUtils.isNotBlank(contentTypeId) && !_cTypeEP.hasExtension(contentTypeId))
1672        {
1673            throw new ConfigurationException("The content attribute of path " + definition.getPath() + " in content type " + getId() + " references a nonexistent content-type: '" + contentTypeId + "'");
1674        }
1675    }
1676    
1677    /**
1678     * Check the mutual reference declaration of the given content attribute definition
1679     * @param definition the definition to check
1680     * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater.
1681     * @throws ConfigurationException if there is a problem with mutual reference declaration
1682     */
1683    protected void _checkMutualReferences(ContentAttributeDefinition definition, int repeaterLevel) throws ConfigurationException
1684    {
1685        String contentTypeId = definition.getContentTypeId();
1686        String invertRelationPath = definition.getInvertRelationPath();
1687
1688        if (StringUtils.isNotEmpty(invertRelationPath))
1689        {
1690            String currentAttributePath = definition.getPath();
1691
1692            if (StringUtils.isEmpty(contentTypeId))
1693            {
1694                throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, a content type is required.");
1695            }
1696
1697            // Do not check the extension existence, this is already done in _checkContentAttribute method
1698            ContentType contentType = _cTypeEP.getExtension(contentTypeId);
1699
1700            if (repeaterLevel > 0 && definition.isMultiple())
1701            {
1702                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.");
1703            }
1704            else if (repeaterLevel >= 2)
1705            {
1706                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.");
1707            }
1708
1709            try
1710            {
1711                ModelItem invertRelationDefinition = contentType.getModelItem(invertRelationPath);
1712
1713                // Ensure that the referenced attribute is of type content.
1714                if (!ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(invertRelationDefinition.getType().getId()))
1715                {
1716                    throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' of type " + contentTypeId + " is not of type Content.");
1717                }
1718
1719                String invertCTypeId = ((ContentAttributeDefinition) invertRelationDefinition).getContentTypeId();
1720                String invertPath = ((ContentAttributeDefinition) invertRelationDefinition).getInvertRelationPath();
1721
1722                // Ensure that the referenced attribute's content type is compatible with the current type.
1723                if (!_cTypeEP.isSameOrDescendant(getId(), invertCTypeId))
1724                {
1725                    throw new ConfigurationException("Mutual relationship: the attribute at path " + invertRelationPath + " of type " + contentTypeId + " references an incompatible type: " + StringUtils.defaultString(invertCTypeId, "<null>"));
1726                }
1727
1728                // Ensure that the referenced attribute references this attribute.
1729                if (!currentAttributePath.equals(invertPath))
1730                {
1731                    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.");
1732                }
1733            }
1734            catch (UndefinedItemPathException e)
1735            {
1736                // Ensure the referenced attribute presence.
1737                throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' doesn't exist for type " + contentTypeId);
1738            }
1739        }
1740    }
1741    
1742    /**
1743     * Resolve the temporary view references
1744     * @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, ...)
1745     */
1746    protected void _resolveViewReferences() throws ConfigurationException
1747    {
1748        for (String viewName : getViewNames())
1749        {
1750            View view = getView(viewName);
1751            _resolveViewReferences(view, viewName);
1752        }
1753    }
1754    
1755    /**
1756     * Resolve the temporary view references in the given {@link ViewItemAccessor}
1757     * @param viewItemAccessor the {@link ViewItemAccessor}
1758     * @param currentViewName the name of the current view (to avoid views referencing themselves)
1759     * @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, ...)
1760     */
1761    protected void _resolveViewReferences(ViewItemAccessor viewItemAccessor, String currentViewName) throws ConfigurationException
1762    {
1763        boolean hasResolvedReferences = false;
1764        List<ViewItem> viewItemsWithResolvedReferences = new ArrayList<>();
1765        for (ViewItem viewItem : viewItemAccessor.getViewItems())
1766        {
1767            if (viewItem instanceof TemporaryViewReference)
1768            {
1769                assert viewItemAccessor instanceof ViewElement;
1770                
1771                ElementDefinition definition = ((ViewElement) viewItemAccessor).getDefinition();
1772                assert definition instanceof ContentAttributeDefinition;
1773                
1774                String contentTypeId = ((ContentAttributeDefinition) definition).getContentTypeId();
1775                if (contentTypeId != null)
1776                {
1777                    Set<String> ancestors = _contentTypesHelper.getAncestors(this.getId());
1778                    if (!(ancestors.contains(contentTypeId) && currentViewName.equals(viewItem.getName())))
1779                    {
1780                        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
1781                        View view = contentType.getView(viewItem.getName());
1782                        if (view != null)
1783                        {
1784                            hasResolvedReferences = true;
1785                            viewItemsWithResolvedReferences.addAll(view.getViewItems());
1786                        }
1787                        else
1788                        {
1789                            throw new ConfigurationException("The view '" + viewItem.getName() + "' does not exist in content type '" + contentTypeId + "' referenced by the attribute named '" + definition.getName() + "'.");
1790                        }
1791                    }
1792                    else
1793                    {
1794                        throw new ConfigurationException("The view '" + viewItem.getName() + "' cannot make a reference to itself.");
1795                    }
1796                }
1797                else
1798                {
1799                    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() + "'.");
1800                }
1801            }
1802            else
1803            {
1804                if (viewItem instanceof ViewItemAccessor)
1805                {
1806                    _resolveViewReferences((ViewItemAccessor) viewItem, currentViewName);
1807                }
1808                viewItemsWithResolvedReferences.add(viewItem);
1809            }
1810        }
1811        
1812        if (hasResolvedReferences)
1813        {
1814            viewItemAccessor.clear();
1815            viewItemAccessor.addViewItems(viewItemsWithResolvedReferences);
1816        }
1817    }
1818    
1819    /**
1820     * Browse the indexing model and compute indexing field references.
1821     */
1822    protected void _computeIndexingModelReferences()
1823    {
1824        // Impacted ContentType -> local IndexingField name -> path to impacted content.
1825        Map<String, Map<String, List<String>>> references = new HashMap<>();
1826        
1827        for (IndexingField field : _indexingModel.getFields())
1828        {
1829            if (field instanceof MetadataIndexingField)
1830            {
1831                String metadataPath = ((MetadataIndexingField) field).getMetadataPath();
1832                
1833                List<MetadataDefinition> definitions = getIndexingFieldDefinitions(this, metadataPath);
1834                
1835                List<String> joinPaths = new ArrayList<>();
1836                boolean localContentType = true;
1837                StringBuilder currentContentPath = new StringBuilder();
1838                for (MetadataDefinition definition : definitions)
1839                {
1840                    if (currentContentPath.length() > 0)
1841                    {
1842                        currentContentPath.append(ContentConstants.METADATA_PATH_SEPARATOR);
1843                    }
1844                    currentContentPath.append(definition.getName());
1845                    
1846                    if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT)
1847                    {
1848                        if (!localContentType)
1849                        {
1850                            joinPaths.add(currentContentPath.toString());
1851                            currentContentPath.setLength(0);
1852                            
1853                            String cTypeId = definition.getContentType();
1854                            
1855                            Map<String, List<String>> cTypeRefs;
1856                            if (references.containsKey(cTypeId))
1857                            {
1858                                cTypeRefs = references.get(cTypeId);
1859                            }
1860                            else
1861                            {
1862                                cTypeRefs = new LinkedHashMap<>();
1863                                references.put(cTypeId, cTypeRefs);
1864                            }
1865                            
1866                            cTypeRefs.put(field.getName(), new ArrayList<>(joinPaths));
1867                        }
1868                        
1869                        localContentType = false;
1870                    }
1871                }
1872            }
1873        }
1874        
1875        _indexingModel.setReferences(references);
1876    }
1877    
1878    /**
1879     * Get the list of metadata definitions "traversed" from the initial content type to the given metadata.
1880     * @param initialContentType the initial content type.
1881     * @param metadataPath the compound metadata path.
1882     * @return the list of metadata definitions.
1883     */
1884    protected List<MetadataDefinition> getIndexingFieldDefinitions(ContentType initialContentType, String metadataPath)
1885    {
1886        List<MetadataDefinition> definitions = new ArrayList<>();
1887        
1888        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1889        
1890        if (pathSegments.length > 0)
1891        {
1892            IndexingModel indexingModel = _contentTypesHelper.getIndexingModel(new String[] {initialContentType.getId()}, new String[0]);
1893            
1894            IndexingField refField = indexingModel.getField(pathSegments[0]);
1895            
1896            MetadataDefinition metadataDef = null;
1897            if (refField != null && refField instanceof CustomMetadataIndexingField)
1898            {
1899                metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition();
1900            }
1901            else
1902            {
1903                metadataDef = initialContentType.getMetadataDefinition(pathSegments[0]);
1904            }
1905            
1906            if (metadataDef != null)
1907            {
1908                definitions.add(metadataDef);
1909            }
1910            
1911            for (int i = 1; i < pathSegments.length && metadataDef != null; i++)
1912            {
1913                if (metadataDef.getType() == MetadataType.CONTENT || metadataDef.getType() == MetadataType.SUB_CONTENT)
1914                {
1915                    String refCTypeId = metadataDef.getContentType();
1916                    if (refCTypeId != null && _cTypeEP.hasExtension(refCTypeId))
1917                    {
1918                        ContentType refCType = _cTypeEP.getExtension(refCTypeId);
1919                        
1920                        List<MetadataDefinition> followingDefs = getIndexingFieldDefinitions(refCType, StringUtils.join(pathSegments, '/', i, pathSegments.length));
1921                        definitions.addAll(followingDefs);
1922                        
1923                        return definitions;
1924                    }
1925                }
1926                else
1927                {
1928                    refField = indexingModel.getField(pathSegments[i]);
1929                    if (refField != null && refField instanceof CustomMetadataIndexingField)
1930                    {
1931                        metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition();
1932                    }
1933                    else
1934                    {
1935                        metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
1936                    }
1937                    definitions.add(metadataDef);
1938                }
1939            }
1940        }
1941        
1942        return definitions;
1943    }
1944    
1945    @Override
1946    public List<ContentValidator> getGlobalValidators()
1947    {
1948        return Collections.unmodifiableList(_globalValidators);
1949    }
1950    
1951    @Override
1952    public RichTextUpdater getRichTextUpdater()
1953    {
1954        return _richTextUpdater;
1955    }
1956    
1957    @Override
1958    public Set<String> getMetadataNames()
1959    {
1960        return Collections.unmodifiableSet(_metadata.keySet());
1961    }
1962
1963    @Override
1964    public MetadataDefinition getMetadataDefinition(String metadataName)
1965    {
1966        return _metadata.get(metadataName);
1967    }
1968    
1969    @Override
1970    public boolean hasMetadataDefinition(String metadataName)
1971    {
1972        return _metadata.containsKey(metadataName);
1973    }
1974    
1975    @Override
1976    public MetadataDefinition getMetadataDefinitionByPath(String metadataPath)
1977    {
1978        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1979        
1980        if (pathSegments.length == 0)
1981        {
1982            return null;
1983        }
1984        
1985        MetadataDefinition metadataDef = _metadata.get(pathSegments[0]);
1986        
1987        for (int i = 1;  i < pathSegments.length && metadataDef != null; i++)
1988        {
1989            metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
1990        }
1991        
1992        return metadataDef;
1993    }
1994    
1995    @Override
1996    public IndexingModel getIndexingModel()
1997    {
1998        return _indexingModel;
1999    }
2000    
2001    @Override
2002    public boolean canRead(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException
2003    {
2004        Restrictions restrictions = _getRestrictionsForPath(metadataDef);
2005        
2006        FirstRestrictionsChecksState state = _restrictedModelItemHelper._doFirstRestrictionsChecks(content, restrictions, true);
2007        if (!FirstRestrictionsChecksState.UNKNOWN.equals(state))
2008        {
2009            return FirstRestrictionsChecksState.TRUE.equals(state);
2010        }
2011        
2012        String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR);
2013        if (pathSegments.length > 1)
2014        {
2015            // Check read access on parent metadata definition
2016            String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR));
2017            MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content);
2018            return canRead(content, parentMetadataDef);
2019        }
2020        
2021        return true;
2022    }
2023
2024    @Override
2025    public boolean canWrite(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException
2026    {
2027        Restrictions restrictions = _getRestrictionsForPath(metadataDef);
2028        
2029        FirstRestrictionsChecksState state = _restrictedModelItemHelper._doFirstRestrictionsChecks(content, restrictions, false);
2030        if (!FirstRestrictionsChecksState.UNKNOWN.equals(state))
2031        {
2032            return FirstRestrictionsChecksState.TRUE.equals(state);
2033        }
2034        
2035        String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR);
2036        if (pathSegments.length > 1)
2037        {
2038            // Check write access on parent metadata definition
2039            String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR));
2040            MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content);
2041            return canWrite(content, parentMetadataDef);
2042        }
2043
2044        return canRead(content, metadataDef);
2045    }
2046    
2047    @Override
2048    public Set<String> getTags()
2049    {
2050        return Collections.unmodifiableSet(_tags);
2051    }
2052    
2053    @Override
2054    public Set<String> getInheritableTags()
2055    {
2056        return Collections.unmodifiableSet(_inheritableTags);
2057    }
2058    
2059    @Override
2060    public boolean hasTag(String tagName)
2061    {
2062        return _tags.contains(tagName);
2063    }
2064    
2065    @Override
2066    public boolean isPrivate()
2067    {
2068        return hasTag(TAG_PRIVATE);
2069    }
2070    
2071    @Override
2072    public boolean isAbstract()
2073    {
2074        return _abstract;
2075    }
2076    
2077    @Override
2078    public boolean isSimple()
2079    {
2080        return _isSimple;
2081    }
2082
2083    @Override
2084    public boolean isReferenceTable()
2085    {
2086        return hasTag(TAG_REFERENCE_TABLE) || hasTag(TAG_RENDERABLE_FERENCE_TABLE);
2087    }
2088    
2089    @Override
2090    public boolean isMultilingual()
2091    {
2092        return _isMultilingual;
2093    }
2094    
2095    @Override
2096    public boolean isMixin()
2097    {
2098        return hasTag(TAG_MIXIN);
2099    }
2100    
2101    @Override
2102    public Set<String> getConfiguredDefaultWorkflowNames()
2103    {
2104        return _configuredWorkflowNames;
2105    }
2106    
2107    @Override
2108    public Optional<String> getDefaultWorkflowName()
2109    {
2110        return _defaultWorkflowName;
2111    }
2112    
2113    @Override
2114    public String getRight()
2115    {
2116        return _right;
2117    }
2118    
2119    @Override
2120    public void saxContentTypeAdditionalData(org.xml.sax.ContentHandler contentHandler, Content content) throws AmetysRepositoryException, SAXException
2121    {
2122        // Nothing
2123    }
2124    
2125    @Override
2126    public Map<String, Object> getAdditionalData(Content content)
2127    {
2128        return new HashMap<>();
2129    }
2130    
2131    /**
2132     * Retrieves the restrictions for a given path.
2133     * @param metadataDef the metadata definition.
2134     * @return the restrictions or <code>null</code> if not found.
2135     */
2136    protected Restrictions _getRestrictionsForPath(MetadataDefinition metadataDef)
2137    {
2138        if (metadataDef != null && metadataDef instanceof RestrictedDefinition)
2139        {
2140            return ((RestrictedDefinition) metadataDef).getRestrictions();
2141        }
2142        
2143        // Not found
2144        return null;
2145    }
2146
2147    @Override
2148    public String toString()
2149    {
2150        return "'" + getId() + "'";
2151    }
2152    
2153    /**
2154     * Restricted definition.
2155     * @deprecated use {@link RestrictedModelItem} instead
2156     */
2157    @Deprecated
2158    protected interface RestrictedDefinition
2159    {
2160        /**
2161         * Provides the restrictions.
2162         * @return the restrictions.
2163         */
2164        Restrictions getRestrictions();
2165    }
2166    
2167    /**
2168     * Definition with semantic annotations
2169     */
2170    protected interface AnnotableDefinition
2171    {
2172        /**
2173         * Provides the semantic annotations
2174         * @return the semantic annotations
2175         */
2176        List<SemanticAnnotation> getSemanticAnnotations();
2177
2178        /**
2179         * Set the semantic annotations
2180         * @param annotations the semantic annotations to set
2181         */
2182        void setSemanticAnnotations(List<SemanticAnnotation> annotations);
2183    }
2184    
2185    /**
2186     * Internal {@link MetadataDefinition} storage contains instances of this class.
2187     * @deprecated Use {@link AttributeDefinition} instead
2188     */
2189    @Deprecated
2190    protected static class RestrictedMetadataDefinition extends MetadataDefinition implements RestrictedDefinition
2191    {
2192        /** Restrictions. */
2193        protected Restrictions _restrictions = new Restrictions();
2194        
2195        @Override
2196        public Restrictions getRestrictions()
2197        {
2198            return _restrictions;
2199        }
2200    }
2201    
2202    /**
2203     * Internal {@link MetadataDefinition} storage contains instances of this class.
2204     * @deprecated Use {@link RichTextAttributeDefinition} instead
2205     */
2206    @Deprecated
2207    protected static class RestrictedRichTextDefinition extends RichTextMetadataDefinition  implements RestrictedDefinition
2208    {
2209        /** Restrictions. */
2210        protected Restrictions _restrictions = new Restrictions();
2211        
2212        @Override
2213        public Restrictions getRestrictions()
2214        {
2215            return _restrictions;
2216        }
2217    }
2218    
2219    /**
2220     * Internal {@link org.ametys.cms.contenttype.RepeaterDefinition} storage contains instances of this class.
2221     * @deprecated Use {@link ContentRestrictedRepeaterDefinition} instead
2222     */
2223    @Deprecated
2224    protected static class RestrictedRepeaterDefinition extends org.ametys.cms.contenttype.RepeaterDefinition implements RestrictedDefinition
2225    {
2226        /** Restrictions. */
2227        protected Restrictions _restrictions = new Restrictions();
2228        
2229        @Override
2230        public Restrictions getRestrictions()
2231        {
2232            return _restrictions;
2233        }
2234    }
2235    
2236    /**
2237     * {@link RestrictedMetadataDefinition} and {@link RestrictedRepeaterDefinition} parser.
2238     * @deprecated Use {@link ContentAttributeDefinitionParser} instead
2239     */
2240    @Deprecated
2241    protected class MetadataAndRepeaterDefinitionParser extends AbstractParameterParser<MetadataDefinition, MetadataType>
2242    {
2243        /** Parent prefix. */
2244        protected String _parentPrefix = "";
2245        
2246        /**
2247         * Creates an {@link MetadataAndRepeaterDefinitionParser}.
2248         * @param enumeratorManager the enumerator component manager.
2249         * @param validatorManager the validator component manager.
2250         */
2251        public MetadataAndRepeaterDefinitionParser(ThreadSafeComponentManager<org.ametys.runtime.parameter.Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
2252        {
2253            super(enumeratorManager, validatorManager);
2254        }
2255        
2256        @Override
2257        protected MetadataDefinition _createParameter(Configuration metadataConfiguration)  throws ConfigurationException
2258        {
2259            String defName = metadataConfiguration.getName();                        
2260                        
2261            if (defName.equals("metadata"))
2262            {
2263                String defType = metadataConfiguration.getAttribute("type");
2264                if (defType.equals("rich-text") || defType.equals("html-rich-text"))
2265                {
2266                    return new RestrictedRichTextDefinition();
2267                } 
2268                else 
2269                {
2270                    return new RestrictedMetadataDefinition();                    
2271                }
2272                
2273            }
2274            else if (defName.equals("repeater"))
2275            {
2276                return new RestrictedRepeaterDefinition();
2277            }
2278
2279            throw new ConfigurationException("Unsupported metadata or repeater configuration", metadataConfiguration);
2280        }
2281        
2282        @Override
2283        protected String _parseId(Configuration metadataConfiguration) throws ConfigurationException
2284        {
2285            String metadataName = metadataConfiguration.getAttribute("name");
2286            
2287            if (!metadataName.matches("^[a-zA-Z]((?!__)[a-zA-Z0-9_-])*$"))
2288            {
2289                throw new ConfigurationException("Invalid metadata name: " + metadataName + ". The metadata name must start with a letter and must contain only letters, digits, underscore or dash characters.", metadataConfiguration);
2290            }
2291            
2292            return _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName;
2293        }
2294        
2295        @Override
2296        protected MetadataType _parseType(Configuration metadataConfiguration) throws ConfigurationException
2297        {
2298            if (metadataConfiguration.getName().equals("repeater"))
2299            {
2300                // A repeater is a composite
2301                return MetadataType.COMPOSITE;
2302            }
2303            else if (metadataConfiguration.getAttribute("type").equals("html-rich-text"))
2304            {
2305                return MetadataType.RICH_TEXT;
2306            }
2307            
2308            try
2309            {
2310                return MetadataType.valueOf(metadataConfiguration.getAttribute("type").toUpperCase().replaceAll("-", "_"));
2311            }
2312            catch (IllegalArgumentException e)
2313            {
2314                throw new ConfigurationException("Invalid type", metadataConfiguration, e);
2315            }
2316        }
2317        
2318        @Override
2319        protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException
2320        {
2321            // Override i18n parsing to use the default catalog (which can be application for automatic content types)
2322            return I18nizableText.parseI18nizableText(config.getChild(name), getDefaultCatalog());
2323        }
2324        
2325        @Override
2326        protected Object _parseDefaultValue(Configuration metadataConfiguration, MetadataDefinition metadataDef)
2327        {
2328            String value;
2329            
2330            Configuration childNode = metadataConfiguration.getChild("default-value", false);
2331            if (childNode == null)
2332            {
2333                value = null;
2334            }
2335            else
2336            {
2337                value = childNode.getValue("");
2338            }
2339            
2340            return value;
2341        }
2342        
2343        @Override
2344        protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration metadataConfiguration, String metadataId, MetadataDefinition metadataDefinition) throws ConfigurationException
2345        {
2346            super._additionalParsing(manager, pluginName, metadataConfiguration, metadataId, metadataDefinition);
2347            String metadataName = metadataConfiguration.getAttribute("name");
2348            
2349            metadataDefinition.setReferenceContentType(_id);
2350            metadataDefinition.setName(metadataName);
2351            metadataDefinition.setMultiple(metadataConfiguration.getAttributeAsBoolean("multiple", false));
2352            // Use default transformer (docbook)
2353            metadataDefinition.setRichTextTransformer(_richTextTransformer);
2354            // Use docbook outgoing consistency extractor
2355            metadataDefinition.setRichTextOutgoingReferencesExtractor(_richTextOutgoingReferencesExtractor);
2356            
2357            if (metadataDefinition instanceof org.ametys.cms.contenttype.RepeaterDefinition)
2358            {
2359                org.ametys.cms.contenttype.RepeaterDefinition repeaterDefinition = (org.ametys.cms.contenttype.RepeaterDefinition) metadataDefinition;
2360                
2361                _parseRepeaterDefinition(manager, pluginName, metadataConfiguration, repeaterDefinition);
2362            }
2363            
2364            if (metadataDefinition instanceof AnnotableDefinition)
2365            {
2366                AnnotableDefinition definitionWithSemAnnotations = (AnnotableDefinition) metadataDefinition;
2367                
2368                _parseDefinitionWithAnnotations(manager, pluginName, metadataConfiguration, definitionWithSemAnnotations);
2369            }
2370            
2371            _restrictedModelItemHelper.populateRestrictions(metadataConfiguration, ((RestrictedDefinition) metadataDefinition).getRestrictions());
2372            
2373            if (metadataDefinition.getType() == MetadataType.CONTENT || metadataDefinition.getType() == MetadataType.SUB_CONTENT)
2374            {
2375                // Content metadata: parse and set the content type restriction.
2376                String contentType = metadataConfiguration.getAttribute("contentType", null);
2377                if (StringUtils.isNotEmpty(contentType))
2378                {
2379                    metadataDefinition.setContentType(contentType);
2380                }
2381                
2382                if (metadataDefinition.getType() == MetadataType.CONTENT)
2383                {
2384                    _parseContentRelations(metadataConfiguration, metadataDefinition, contentType);
2385                }
2386            }
2387            else if (metadataDefinition.getType() == MetadataType.COMPOSITE)
2388            {
2389                for (Configuration childConfig : metadataConfiguration.getChildren())
2390                {
2391                    String childName = childConfig.getName();
2392                    
2393                    if (childName.equals("metadata") || childName.equals("repeater"))
2394                    {
2395                        String oldParentPrefix = _parentPrefix;
2396                        // Stack new parent prefix
2397                        _parentPrefix = _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName;
2398                        MetadataDefinition subMetaDef = parseParameter(manager, pluginName, childConfig);
2399                        // Restore parent prefix
2400                        _parentPrefix = oldParentPrefix;
2401                        
2402                        if (!metadataDefinition.addMetadata(subMetaDef))
2403                        {
2404                            throw new ConfigurationException("Metadata with name " + subMetaDef.getName() + " is already defined", childConfig);
2405                        }
2406                    }
2407                }
2408            }
2409        }
2410        
2411        /**
2412         * Parse content mutual relations.
2413         * @param metadataConfiguration the metadata configuration.
2414         * @param metadataDefinition the metadata definition to fill.
2415         * @param contentType the content type.
2416         */
2417        protected void _parseContentRelations(Configuration metadataConfiguration, MetadataDefinition metadataDefinition, String contentType)
2418        {
2419            String invert = metadataConfiguration.getAttribute("invert", null);
2420            if (StringUtils.isNotEmpty(invert))
2421            {
2422                metadataDefinition.setInvertRelationPath(invert);
2423                
2424                boolean forceInvert = metadataConfiguration.getAttributeAsBoolean("forceInvert", false);
2425                metadataDefinition.setForceInvert(forceInvert);
2426            }
2427        }
2428        
2429        /**
2430         * Parses the repeater definition. 
2431         * @param manager the service manager.
2432         * @param pluginName the plugin name declaring this parameter.
2433         * @param metadataConfiguration the metadata configuration to use.
2434         * @param repeaterDefinition the repeater definition.
2435         * @throws ConfigurationException if the configuration is not valid.
2436         */
2437        protected void _parseRepeaterDefinition(ServiceManager manager, String pluginName, Configuration metadataConfiguration, org.ametys.cms.contenttype.RepeaterDefinition repeaterDefinition) throws ConfigurationException
2438        {
2439            repeaterDefinition.setAddLabel(_parseI18nizableText(metadataConfiguration, pluginName, "add-label"));
2440            repeaterDefinition.setDeleteLabel(_parseI18nizableText(metadataConfiguration, pluginName, "del-label"));
2441            repeaterDefinition.setHeaderLabel(metadataConfiguration.getChild("header-label").getValue(null));
2442            repeaterDefinition.setInitialSize(metadataConfiguration.getAttributeAsInteger("initial-size", 0));
2443            repeaterDefinition.setMinSize(metadataConfiguration.getAttributeAsInteger("min-size", 0));
2444            repeaterDefinition.setMaxSize(metadataConfiguration.getAttributeAsInteger("max-size", -1));
2445        }
2446        
2447        /**
2448         * Parses the definition with semantic annotations. 
2449         * @param manager the service manager.
2450         * @param pluginName the plugin name declaring this parameter.
2451         * @param metadataConfiguration the metadata configuration to use.
2452         * @param annotableDefinition the metadata definition
2453         * @throws ConfigurationException if the configuration is not valid.
2454         */
2455        protected void _parseDefinitionWithAnnotations(ServiceManager manager, String pluginName, Configuration metadataConfiguration, AnnotableDefinition annotableDefinition) throws ConfigurationException
2456        {            
2457            Configuration annotationsConfiguration = metadataConfiguration.getChild("annotations");
2458            List<SemanticAnnotation> semAnnotations = _parseSemAnnotations(pluginName, annotationsConfiguration);
2459            annotableDefinition.setSemanticAnnotations(semAnnotations);
2460        }
2461        
2462        /**
2463         * Extract the list of the declared annotations
2464         * @param pluginName the plugin name declaring this parameter.
2465         * @param annotationsConfiguration the annotations configuration to use.
2466         * @return the list of the declared annotations
2467         * @throws ConfigurationException if the configuration is not valid.
2468         */
2469        protected List<SemanticAnnotation> _parseSemAnnotations(String pluginName, Configuration annotationsConfiguration)  throws ConfigurationException
2470        {
2471            List<SemanticAnnotation> annotations = new ArrayList<>();
2472            
2473            for (Configuration annotationConfig : annotationsConfiguration.getChildren("annotation"))
2474            {
2475                String id = annotationConfig.getAttribute("name");
2476                
2477                if (!_getAnnotationNamePattern().matcher(id).matches())
2478                {
2479                    throw new ConfigurationException("Invalid annonation name '" + id + "'. This value is not permitted: only [a-zA-Z][a-zA-Z0-9-_]* are allowed.");
2480                }
2481                
2482                I18nizableText label = _parseI18nizableText(annotationConfig, pluginName, "label");
2483                I18nizableText description = _parseI18nizableText(annotationConfig, pluginName, "description");
2484                annotations.add(new SemanticAnnotation(id, label, description));
2485            }
2486            
2487            return annotations;
2488        }
2489        
2490        /**
2491         * Get the annotation name pattern to test validity.
2492         * @return The annotation name pattern.
2493         */
2494        protected Pattern _getAnnotationNamePattern()
2495        {
2496            if (__annotationNamePattern == null)
2497            {
2498                // [a-zA-Z][a-zA-Z0-9_]*
2499                __annotationNamePattern = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE);
2500            }
2501
2502            return __annotationNamePattern;
2503        }
2504    }
2505    
2506    public Optional<ContentAttributeDefinition> getParentAttributeDefinition()
2507    {
2508        return Optional.ofNullable(_parentAttributeDefinition);
2509    }
2510    
2511    public Collection<ModelItem> getModelItems()
2512    {
2513        return Collections.unmodifiableCollection(_modelItems.values());
2514    }
2515    
2516    public Set<String> getViewNames(boolean includeInternals)
2517    {
2518        if (includeInternals)
2519        {
2520            return Collections.unmodifiableSet(_views.keySet());
2521        }
2522        else
2523        {
2524            return _views.entrySet()
2525                         .stream()
2526                         .filter(entry -> !entry.getValue().isInternal())
2527                         .map(Map.Entry::getKey)
2528                         .collect(Collectors.toSet());
2529        }
2530    }
2531    
2532    public View getView(String viewName)
2533    {
2534        return _views.get(viewName);
2535    }
2536    
2537    public Optional<Configuration> getViewConfiguration(String viewName)
2538    {
2539        return Optional.ofNullable(_viewConfigurations.get(viewName));
2540    }
2541    
2542    public String getFamilyId()
2543    {
2544        return ContentTypeExtensionPoint.ROLE;
2545    }
2546}