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