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