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