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.repository.Content;
060import org.ametys.cms.repository.WorkflowAwareContent;
061import org.ametys.cms.transformation.RichTextTransformer;
062import org.ametys.cms.transformation.docbook.DocbookOutgoingReferencesExtractor;
063import org.ametys.cms.transformation.docbook.DocbookTransformer;
064import org.ametys.core.right.RightManager;
065import org.ametys.core.right.RightManager.RightResult;
066import org.ametys.core.user.CurrentUserProvider;
067import org.ametys.core.user.UserIdentity;
068import org.ametys.plugins.repository.AmetysRepositoryException;
069import org.ametys.plugins.workflow.support.WorkflowProvider;
070import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
071import org.ametys.runtime.i18n.I18nizableText;
072import org.ametys.runtime.parameter.AbstractParameterParser;
073import org.ametys.runtime.parameter.Enumerator;
074import org.ametys.runtime.parameter.StaticEnumerator;
075import org.ametys.runtime.parameter.Validator;
076import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
077
078import com.google.common.collect.HashMultimap;
079import com.google.common.collect.Multimap;
080import com.opensymphony.workflow.spi.Step;
081
082/**
083 * Type of content which is retrieved from a XML configuration.
084 * TODO document xml configuration
085 * ...
086 * Provides access based on rights and current workflow steps.<p>
087 * It used a configuration file with the following format:
088 * <code><br>
089 * &nbsp;&nbsp;&lt;restrict-to&gt;<br>
090 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;right type="read|write" id="RIGHT_ID"/&gt;]*
091 * &nbsp;&nbsp;&lt;!-- logical OR between several right id of the same type --&gt;<br>
092 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;workflow type="read|write" step="3"/&gt;]*
093 * &nbsp;&nbsp;&lt;!-- logical OR between several workflow step of the same type --&gt;<br>
094 * &nbsp;&nbsp;&nbsp;&nbsp;[&lt;cannot type="read|write"/&gt;]*<br>
095 * &nbsp;&nbsp;&lt;/restrict-to&gt;<br>
096 * </code>
097 */
098public class DefaultContentType extends AbstractContentTypeDescriptor implements ContentType, Contextualizable, ThreadSafe, Disposable
099{
100    /** Suffix for global validator role. */
101    protected static final String __GLOBAL_VALIDATOR_ROLE_PREFIX = "_globalvalidator";
102    
103    static Pattern __annotationNamePattern;
104    
105    /** Metadata definitions. */
106    protected Map<String, MetadataDefinition> _metadata = new LinkedHashMap<>();
107    /** The right needed to create a content of this type, or null if no right is needed. */
108    protected String _right;
109    /** The abstract property */
110    protected boolean _abstract;
111    /** The tags */
112    protected Set<String> _tags;
113    /** The parent metadata name */
114    protected String _parentMetadataName;
115    /** Service manager. */
116    protected ServiceManager _manager;
117    /** Avalon Context. */
118    protected Context _context;
119    /** Cocoon Context */
120    protected org.apache.cocoon.environment.Context _cocoonContext;
121    /** The workflow provider */
122    protected WorkflowProvider _workflowProvider;
123    /** The rights manager. */
124    protected RightManager _rightManager;
125    /** Current user provider. */
126    protected CurrentUserProvider _currentUserProvider;
127    /** Default rich text transformer. */
128    protected RichTextTransformer _richTextTransformer;
129    /** Docbook (rich text) outgoing references extractor. */
130    protected RichTextOutgoingReferencesExtractor _richTextOutgoingReferencesExtractor;
131    /** Potential global validators. */
132    protected List<ContentValidator> _globalValidators;
133    /** Potentiel richtext updater */
134    protected RichTextUpdater _richTextUpdater;
135    /** Indexing model */
136    protected IndexingModel _indexingModel;
137    /** The helper component for hierarchical simple contents */
138    protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper;
139    
140    // ComponentManager pour les Validator
141    private ThreadSafeComponentManager<Validator> _validatorManager;
142    
143    // ComponentManager pour les Global Validators
144    private ThreadSafeComponentManager<ContentValidator> _globalValidatorsManager;
145    
146    //ComponentManager pour les Enumerator
147    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
148    
149    // ComponentManager pour les CustomIndexingField
150    private ThreadSafeComponentManager<CustomIndexingField> _customFieldManager;
151
152    // ComponentManager pour les CustomMetadataIndexingField
153    private ThreadSafeComponentManager<CustomMetadataIndexingField> _customMetadataIndexingFieldManager;
154
155    private boolean _isSimple;
156
157    private boolean _isMultilingual;
158    
159    @Override
160    public void service(ServiceManager manager) throws ServiceException
161    {
162        super.service(manager);
163        _manager = manager;
164        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
165        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
166        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
167        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
168        _richTextOutgoingReferencesExtractor = (RichTextOutgoingReferencesExtractor) manager.lookup(DocbookOutgoingReferencesExtractor.ROLE);
169        _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE);
170        _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
171    }
172
173    @Override
174    public void contextualize(Context context) throws ContextException
175    {
176        _context = context;
177        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
178    }
179    
180    @Override
181    public void dispose()
182    {
183        _validatorManager.dispose();
184        _validatorManager = null;
185        
186        _globalValidatorsManager.dispose();
187        _globalValidatorsManager = null;
188        
189        _enumeratorManager.dispose();
190        _enumeratorManager = null;
191        
192        _customFieldManager.dispose();
193        _customFieldManager = null;
194        
195        _customMetadataIndexingFieldManager.dispose();
196        _customMetadataIndexingFieldManager = null;
197    }
198
199    @Override
200    protected Configuration getRootConfiguration(Configuration configuration)
201    {
202        return configuration.getChild("content-type");
203    }
204    
205    @Override
206    protected Configuration getOverridenConfiguration() throws ConfigurationException
207    {
208        Configuration overridenConf = null;
209        File ctFile = new File(_cocoonContext.getRealPath("/WEB-INF/param/content-types/_override/" + _id + ".xml"));
210        
211        if (ctFile.exists())
212        {
213            try (InputStream is = new FileInputStream(ctFile))
214            {
215                 
216                overridenConf = new DefaultConfigurationBuilder(true).build(is);
217            }
218            catch (Exception ex)
219            {
220                throw new ConfigurationException("Unable to parse overriden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", overridenConf, ex);
221            }
222        }
223        
224        return overridenConf;
225    }
226    
227    @Override
228    public void configure(Configuration configuration) throws ConfigurationException
229    {
230        _validatorManager = new ThreadSafeComponentManager<>();
231        _validatorManager.setLogger(getLogger());
232        _validatorManager.contextualize(_context);
233        _validatorManager.service(_manager);
234        
235        _globalValidatorsManager = new ThreadSafeComponentManager<>();
236        _globalValidatorsManager.setLogger(getLogger());
237        _globalValidatorsManager.contextualize(_context);
238        _globalValidatorsManager.service(_manager);
239        
240        _enumeratorManager = new ThreadSafeComponentManager<>();
241        _enumeratorManager.setLogger(getLogger());
242        _enumeratorManager.contextualize(_context);
243        _enumeratorManager.service(_manager);
244        
245        _customFieldManager = new ThreadSafeComponentManager<>();
246        _customFieldManager.setLogger(getLogger());
247        _customFieldManager.contextualize(_context);
248        _customFieldManager.service(_manager);
249        
250        _customMetadataIndexingFieldManager = new ThreadSafeComponentManager<>();
251        _customMetadataIndexingFieldManager.setLogger(getLogger());
252        _customMetadataIndexingFieldManager.contextualize(_context);
253        _customMetadataIndexingFieldManager.service(_manager);
254        
255        Configuration rootConfiguration = getRootConfiguration(configuration);
256        
257        _abstract = rootConfiguration.getAttributeAsBoolean("abstract", false);
258        
259        _configureSuperTypes(rootConfiguration);
260        
261        _configureLabels(rootConfiguration);
262        _configureIcons(rootConfiguration);
263        
264        _configureCSSFiles(rootConfiguration);
265        
266        // Tags
267        _tags = new HashSet<>();
268        
269        if (rootConfiguration.getChild("tags", false) != null)
270        {
271            if (rootConfiguration.getChild("tags").getAttributeAsBoolean("inherited", false))
272            {
273                // Get tags from super types
274                for (String superTypeId : _superTypeIds)
275                {
276                    ContentType superType = _cTypeEP.getExtension(superTypeId);
277                    _tags.addAll(superType.getTags());
278                }
279            }
280            _tags.addAll(_parseTags (rootConfiguration.getChild("tags")));
281        }
282        
283        // Rights
284        _right = rootConfiguration.getChild("right").getValue(null);
285        
286        _isSimple = true;
287        for (String superTypeId : _superTypeIds)
288        {
289            ContentType superType = _cTypeEP.getExtension(superTypeId);
290            if (!superType.isSimple())
291            {
292                _isSimple = false;
293                break;
294            }
295        }
296        
297        _isMultilingual = false;
298        for (String superTypeId : _superTypeIds)
299        {
300            ContentType superType = _cTypeEP.getExtension(superTypeId);
301            if (superType.isMultilingual())
302            {
303                _isMultilingual = true;
304                break;
305            }
306        }
307        
308        // Metadata definitions
309        _configureMetadataDefinitions (rootConfiguration);
310        
311        // Parent content type
312        _configureParentContentType(rootConfiguration);
313        
314        // Metadata sets
315        _configureMetadataSets (rootConfiguration);
316        
317        if (!_abstract && !hasTag(TAG_MIXIN))
318        {
319            if (!_allMetadataSetsForView.containsKey("details"))
320            {
321                throw new ConfigurationException("Mandatory metadata-set for view named 'details' is missing for content type " + _id);
322            }
323            if (!_allMetadataSetsForView.containsKey("main"))
324            {
325                throw new ConfigurationException("Mandatory metadata-set for view named 'main' is missing for content type " + _id);
326            }
327            if (!_allMetadataSetsForEdition.containsKey("main"))
328            {
329                throw new ConfigurationException("Mandatory metadata-set for edition named 'main' is missing for content type " + _id);
330            }
331        }
332        
333        // Global validators
334        _configureGlobalValidators (rootConfiguration);
335        
336        // Indexing model
337        _configureIndexingModel (rootConfiguration);
338    }
339    
340    /**
341     * Configure metadata definitions
342     * @param mainConfig The content type configuration
343     * @throws ConfigurationException if an error occurred
344     */
345    protected void _configureMetadataDefinitions (Configuration mainConfig) throws ConfigurationException
346    {
347        MetadataAndRepeaterDefinitionParser defParser = new MetadataAndRepeaterDefinitionParser(_enumeratorManager, _validatorManager);
348        
349        // First, get metadata from super type if applicable.
350        _metadata.putAll(_contentTypesHelper.getMetadataDefinitions(_superTypeIds));
351        
352        Map<String, Configuration> metadataConfiguration = new LinkedHashMap<>();
353        
354        _getApplicableMetadata(mainConfig, metadataConfiguration, false);
355        
356        Configuration overriddenConfig = getOverridenConfiguration();
357        if (overriddenConfig != null)
358        {
359            _getApplicableMetadata(overriddenConfig, metadataConfiguration, true);
360        }
361        
362        // Then, parse own metadata
363        _parseAllMetadatas(metadataConfiguration, defParser);
364        
365        try
366        {
367            defParser.lookupComponents();
368        }
369        catch (Exception e)
370        {
371            throw new ConfigurationException("Unable to lookup parameter local components", overriddenConfig, e);
372        }
373        
374    }
375    
376    /**
377     * Configures the "parent" content type. 
378     * This must not be confounded with the super types. (See {@link AbstractContentTypeDescriptor#_configureSuperTypes(Configuration)})
379     * @param mainConfig The main configuration
380     * @throws ConfigurationException if an error occured
381     */
382    protected void _configureParentContentType(Configuration mainConfig) throws ConfigurationException
383    {
384        Configuration parentCTypeConf = mainConfig.getChild("parent-ref", false);
385        if (parentCTypeConf != null)
386        {
387            // Check this content type is a reference table
388            if (!isReferenceTable())
389            {
390                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());
391                return;
392            }
393            
394            String refMetadataName = parentCTypeConf.getAttribute("name");
395            // Check valid reference of metadata
396            if (!_metadata.containsKey(refMetadataName))
397            {
398                getLogger().error("The 'parent-ref' tag for content type '{}' references the metadata '{}' but it does not exist. It will be ignored.", getId(), refMetadataName);
399                return;
400            }
401            
402            MetadataDefinition metadataDef = _metadata.get(refMetadataName);
403            // Check metadata of type "content"
404            if (!MetadataType.CONTENT.equals(metadataDef.getType()))
405            {
406                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);
407                return;
408            }
409            else if (metadataDef.isMultiple())
410            {
411                getLogger().error("The 'parent-ref' tag for content type '{}' references the metadata '{}' but it is a multiple metadata. It will be ignored.", getId(), refMetadataName);
412                return;
413            }
414            
415            String parentCTypeName = metadataDef.getContentType();
416            ContentType parentCType = _cTypeEP.getExtension(parentCTypeName);
417            
418            if (parentCType == null)
419            {
420                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);
421                return;
422            }
423            // Check parent content type is private AND simple
424            else if (!parentCType.isPrivate())
425            {
426                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);
427                return;
428            }
429            else if (!parentCType.isReferenceTable())
430            {
431                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);
432                return;
433            }
434            
435            if (!_hierarchicalSimpleContentsHelper.registerRelation(parentCType, this))
436            {
437                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());
438                return;
439            }
440            _parentMetadataName = refMetadataName;
441        }
442    }
443    
444    /**
445     * Configure the global validators for content type
446     * @param config The content type configuration
447     * @throws ConfigurationException if an error occured
448     */
449    @SuppressWarnings("unchecked")
450    protected void _configureGlobalValidators (Configuration config) throws ConfigurationException
451    {
452        _globalValidators = new ArrayList<>();
453        List<String> globalValidatorsToLookup = new ArrayList<>();
454        
455        Configuration globalValidatorsConfig = config.getChild("global-validators", true);
456        
457        boolean includeSuperTypeValidators = globalValidatorsConfig.getAttributeAsBoolean("include-from-supertype", true);
458        if (includeSuperTypeValidators)
459        {
460            for (String superTypeId : _superTypeIds)
461            {
462                ContentType cType = _cTypeEP.getExtension(superTypeId);
463                _globalValidators.addAll(cType.getGlobalValidators());
464            }
465        }
466        
467        int count = 1;
468        for (Configuration globalValidatorConfig : globalValidatorsConfig.getChildren("global-validator"))
469        {
470            String globalValidatorId = __GLOBAL_VALIDATOR_ROLE_PREFIX + count++;
471            String validatorClassName = globalValidatorConfig.getAttribute("class");
472            
473            try
474            {
475                Class validatorClass = Class.forName(validatorClassName);
476                _globalValidatorsManager.addComponent(_pluginName, null, globalValidatorId, validatorClass, globalValidatorConfig);
477            }
478            catch (Exception e)
479            {
480                throw new ConfigurationException("Unable to instantiate global validator for class: " + validatorClassName, e);
481            }
482            
483            globalValidatorsToLookup.add(globalValidatorId);
484        }
485        
486        try
487        {
488            _globalValidatorsManager.initialize();
489        }
490        catch (Exception e)
491        {
492            throw new ConfigurationException("Unable to initialize global validator manager", e);
493        }
494        
495        for (String validatorRole : globalValidatorsToLookup)
496        {
497            try
498            {
499                ContentValidator contentValidator = _globalValidatorsManager.lookup(validatorRole);
500                contentValidator.setContentType(this);
501                
502                _globalValidators.add(contentValidator);
503            }
504            catch (ComponentException e)
505            {
506                throw new ConfigurationException("Unable to lookup global validator role: '" + validatorRole + "' for content type: " + this.getId(), e);
507            }
508        }
509        
510    }
511    
512    /**
513     * Fill a map of the applicable metadata configurations.
514     * @param config the content type configuration.
515     * @param metadataConfigurations the Map of metadata {@link Configuration}, indexed by name.
516     * @param allowOverride if true, encountering a metadata which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 
517     * @throws ConfigurationException if an error occurs.
518     */
519    protected void _getApplicableMetadata(Configuration config, Map<String, Configuration> metadataConfigurations, boolean allowOverride) throws ConfigurationException
520    {
521        for (Configuration childConfiguration : config.getChildren())
522        {
523            String childName = childConfiguration.getName();
524            
525            if (childName.equals("metadata") || childName.equals("repeater"))
526            {
527                String metadataName = childConfiguration.getAttribute("name", "");
528                
529                if (!allowOverride && metadataConfigurations.containsKey(metadataName))
530                {
531                    throw new ConfigurationException("Metadata with name '" + metadataName + "' is already defined", childConfiguration);
532                }
533                else if (allowOverride && metadataConfigurations.containsKey(metadataName))
534                {
535                    _checkMetadataTypes (metadataConfigurations.get(metadataName), childConfiguration);
536                }
537                
538                metadataConfigurations.put(metadataName, childConfiguration);
539            }
540            else if (childName.equals("dublin-core"))
541            {
542                metadataConfigurations.put("dc", childConfiguration);
543            }
544        }
545    }
546    
547    /**
548     * Parse all metadata configurations.
549     * @param metadataConfigurations the metadata configurations.
550     * @param defParser the metadata definition parser.
551     * @throws ConfigurationException if the configuration is invalid.
552     */
553    protected void _parseAllMetadatas(Map<String, Configuration> metadataConfigurations, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
554    {
555        for (Configuration childConfiguration : metadataConfigurations.values())
556        {
557            String childName = childConfiguration.getName();
558            
559            if (childName.equals("metadata") || childName.equals("repeater"))
560            {
561                _parseMetadata(childConfiguration, defParser);
562            }
563            else if (childName.equals("dublin-core"))
564            {
565                _parseDublinCoreMetadata(defParser);
566            }
567        }
568    }
569    
570    /**
571     * Parse a metadata configuration.
572     * @param metadataConfiguration the metadata configuration.
573     * @param defParser the metadata definition parser.
574     * @return the created MetadataDefinition.
575     * @throws ConfigurationException if the configuration is invalid
576     */
577    protected MetadataDefinition _parseMetadata(Configuration metadataConfiguration, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
578    {
579        MetadataDefinition metadataDefinition = defParser.parseParameter(_manager, _pluginName, metadataConfiguration);
580        metadataDefinition.setReferenceContentType(_id);
581        
582        String metadataName = metadataDefinition.getName();
583        
584        if (_metadata.containsKey(metadataName))
585        {
586            _checkMetadataTypes (_metadata.get(metadataName), metadataDefinition);
587        }
588        
589        // Update simple and multilingual properties
590        _checkMetadataDefinition(metadataDefinition);
591        
592        _metadata.put(metadataName, metadataDefinition);
593        
594        return metadataDefinition;
595    }
596    
597    /**
598     * Check if all metadata's types defined in first metadata definition are equals to those defined in second metadata definition
599     * @param metaDef1 The first metadata definition to compare
600     * @param metaDef2 The second metadata definition to compare
601     * @throws ConfigurationException if the types are not equals
602     */
603    protected void _checkMetadataTypes (MetadataDefinition metaDef1, MetadataDefinition metaDef2) throws ConfigurationException
604    {
605        if (!metaDef1.getType().equals(metaDef2.getType()))
606        {
607            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() + "'");
608        }
609        
610        if (metaDef1.getType().equals(MetadataType.COMPOSITE))
611        {
612            for (String subMetadataName : metaDef1.getMetadataNames())
613            {
614                if (metaDef2.getMetadataDefinition(subMetadataName) != null)
615                {
616                    _checkMetadataTypes (metaDef1.getMetadataDefinition(subMetadataName), metaDef2.getMetadataDefinition(subMetadataName));
617                 
618                    // Update simple and multilingual properties
619                    _checkMetadataDefinition(metaDef1.getMetadataDefinition(subMetadataName));
620                }
621            }
622        }
623    }
624    
625    /**
626     * Check if all metadata's types defined in first configuration are equals to those defined in second configuration
627     * @param metadataConf1 The first configuration to compare
628     * @param metadataConf2 The second configuration to compare
629     * @throws ConfigurationException if the types are not equals
630     */
631    protected void _checkMetadataTypes (Configuration metadataConf1, Configuration metadataConf2) throws ConfigurationException
632    {
633        String type = metadataConf1.getAttribute("type", "");
634        String overridenType = metadataConf2.getAttribute("type", "");
635        if (!overridenType.equals(type))
636        {
637            throw new ConfigurationException("The type of metadata '" + metadataConf1.getAttribute("name") + " (" + type.toUpperCase() + ")" + "' can not be overriden to '" + metadataConf2.getAttribute("name") + " (" + overridenType.toUpperCase() + ")'");
638        }
639        
640        if ("composite".equals(type) || metadataConf1.getName().equals("repeater"))
641        {
642            for (Configuration childConfig1 : metadataConf1.getChildren())
643            {
644                String childName = childConfig1.getName();
645                if (childName.equals("metadata") || childName.equals("repeater"))
646                {
647                    Configuration childConfig2 = null;
648                    for (Configuration conf : metadataConf2.getChildren(childName))
649                    {
650                        if (childConfig1.getAttribute("name").equals(conf.getAttribute("name")))
651                        {
652                            childConfig2 = conf;
653                            break;
654                        }
655                    }
656                    
657                    if (childConfig2 != null)
658                    {
659                        _checkMetadataTypes (childConfig1, childConfig2);
660                    }
661                }
662            }
663        }
664    }
665    
666    /**
667     * Check the medatata definition to determines if this content type is multilingual and/or simple
668     * All medatata of a simple content-type have to be a simple type (string, long, date, ..)
669     * A multilingual content type should contain at least a metadata of type MULTILINGUAL_STRING
670     * @param metadataDefinition The metadata definition
671     * @return false if the medatata definition is not a valid medatata definition for a simple content-type
672     */
673    protected boolean _checkMetadataDefinition (MetadataDefinition metadataDefinition)
674    {
675        MetadataType type = metadataDefinition.getType();
676        
677        switch (type)
678        {
679            case MULTILINGUAL_STRING:
680                // The content type is a multilingual content type
681                _isMultilingual = true;
682                break;
683                
684            case COMPOSITE:
685            case FILE:
686            case GEOCODE:
687            case REFERENCE:
688            case RICH_TEXT:
689                // The content type can not be simple (complex metadata are not allowed)
690                _isSimple = false;
691                break;
692            default:
693                break;
694        }
695        
696        return true;
697    }
698    
699    /**
700     * Parse DublinCore metadata
701     * @param defParser The parser definition
702     * @throws ConfigurationException if the configuration is invalid
703     */
704    protected void _parseDublinCoreMetadata (MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
705    {
706        Source src = null;
707        
708        try
709        {
710            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
711            
712            if (src.exists())
713            {
714                Configuration configuration = null;
715                try (InputStream is = src.getInputStream())
716                {
717                    configuration = new DefaultConfigurationBuilder(true).build(is);
718                }
719                
720                MetadataDefinition metadataDefinition = new MetadataDefinition();
721                metadataDefinition.setReferenceContentType(_id);
722                metadataDefinition.setId("/dc"); // FIXME ?
723                metadataDefinition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL"));
724                metadataDefinition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC"));
725                metadataDefinition.setName("dc");
726                metadataDefinition.setType(MetadataType.COMPOSITE);
727                
728                for (Configuration childConfiguration : configuration.getChildren())
729                {
730                    String childName = childConfiguration.getName();
731                    
732                    if (childName.equals("metadata"))
733                    {
734                        MetadataDefinition metaDef = defParser.parseParameter(_manager, _pluginName, childConfiguration);
735                        metaDef.setReferenceContentType(_id);
736                        String metadataName = metaDef.getName();
737                        metaDef.setId("/dc/" + metadataName);  // FIXME ?
738                        
739                        if (metaDef.getEnumerator() == null && _dcProvider.isEnumerated(metadataName))
740                        {
741                            StaticEnumerator enumerator = new StaticEnumerator();
742                            
743                            Map<String, I18nizableText> entries = _dcProvider.getEntries(metadataName);
744                            if (entries != null)
745                            {
746                                for (String value : entries.keySet())
747                                {
748                                    enumerator.add(entries.get(value), value);
749                                }
750                               
751                            }
752                            metaDef.setEnumerator(enumerator);
753                        }
754                        
755                        metadataDefinition.addMetadata(metaDef);
756                    }
757                }
758                
759                _metadata.put("dc", metadataDefinition);
760            }
761        }
762        catch (IOException e)
763        {
764            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
765        }
766        catch (SAXException e)
767        {
768            throw new ConfigurationException("Unable to parse Dublin Core metadata", e);
769        }
770        finally
771        {
772            if (src != null)
773            {
774                _srcResolver.release(src);
775            }
776        }
777    }
778    
779    /**
780     * Configure the indexing model
781     * @param config The main configuration
782     * @throws ConfigurationException if an error occurred
783     */
784    protected void _configureIndexingModel (Configuration config) throws ConfigurationException
785    {
786        Configuration indexConf = config.getChild("indexing-model", true);
787        
788        _indexingModel = new IndexingModel();
789        
790        boolean includeFromSuperType = indexConf.getAttributeAsBoolean("include-from-supertype", true);
791        if (includeFromSuperType)
792        {
793            _indexingModel = _contentTypesHelper.getIndexingModel(_superTypeIds, new String[0]);
794        }
795        
796        boolean includeAll = indexConf.getAttributeAsBoolean("include-all", true);
797        if (includeAll)
798        {
799            for (String metadataName : _metadata.keySet())
800            {
801                MetadataDefinition definition = _metadata.get(metadataName);
802                _indexingModel.addIndexingField(new DefaultMetadataIndexingField(metadataName, definition, metadataName));
803            }
804        }
805        
806        // Optionally add the semantic annotations to the indexing model.
807        boolean includeSemanticAnnotations = indexConf.getAttributeAsBoolean("include-semantic-annotations", true);
808        if (includeSemanticAnnotations)
809        {
810            _addSemanticAnnotations(indexConf, this);
811        }
812        
813        // Metadata fields.
814        _configureMetadataIndexingFields(indexConf);
815        
816        // Custom fields.
817        _configureCustomIndexingFields(indexConf);
818        
819        // Custom metadata fields.
820        _configureCustomMetadataIndexingFields(indexConf);
821    }
822    
823    /**
824     * Add semantic annotations as indexing fields to the indexing model.
825     * @param indexConf the indexing model configuration.
826     * @param holder the metadata holder (ContentType or MetadataDefinition) to scan for annotable metadata.
827     */
828    protected void _addSemanticAnnotations(Configuration indexConf, MetadataDefinitionHolder holder)
829    {
830        // Get the semantic annotations in a multimap.
831        Multimap<SemanticAnnotation, String> metadatas = HashMultimap.create();
832        _getSemanticAnnotations(holder, metadatas, "");
833        
834        // Add a custom indexing field for each annotation to the indexing model.
835        for (SemanticAnnotation annotation : metadatas.keySet())
836        {
837            Collection<String> metaPaths = metadatas.get(annotation);
838            _indexingModel.addIndexingField(new SemanticAnnotationIndexingField(annotation, metaPaths, this));
839        }
840    }
841    
842    /**
843     * Get the semantic annotations and their paths from the given metadata definition holder's sub-tree.
844     * @param holder The current metadata definition holder.
845     * @param annotations The map of semantic annotations and the corresponding metadata paths.
846     * @param prefix The current prefix in the metadata tree (with a trailing slash, if applicable).
847     */
848    protected void _getSemanticAnnotations(MetadataDefinitionHolder holder, Multimap<SemanticAnnotation, String> annotations, String prefix)
849    {
850        for (String metadataName : holder.getMetadataNames())
851        {
852            String metaPath = prefix + metadataName;
853            MetadataDefinition metaDef = holder.getMetadataDefinition(metadataName);
854            if (metaDef instanceof AnnotableDefinition)
855            {
856                List<SemanticAnnotation> metaAnnotations = ((AnnotableDefinition) metaDef).getSemanticAnnotations();
857                for (SemanticAnnotation annotation : metaAnnotations)
858                {
859                    annotations.put(annotation, metaPath);
860                }
861            }
862            
863            _getSemanticAnnotations(metaDef, annotations, metaPath + ContentConstants.METADATA_PATH_SEPARATOR);
864        }
865    }
866    
867    /**
868     * Configure the metadata indexing fields.
869     * @param indexConf the indexing model configuration.
870     * @throws ConfigurationException if an error occurs.
871     */
872    protected void _configureMetadataIndexingFields(Configuration indexConf) throws ConfigurationException
873    {
874        Configuration[] fieldsConf = indexConf.getChildren("metadata-field");
875        for (Configuration fieldConf : fieldsConf)
876        {
877            String metadataPath = fieldConf.getAttribute("path");
878            
879            MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(metadataPath, this);
880            if (metadataDef != null)
881            {
882                String fieldName = fieldConf.getAttribute("name", null);
883                if (fieldName == null)
884                {
885                    fieldName = StringUtils.substringBeforeLast(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
886                }
887                
888                // TODO check if metadataDef.getType() is a primitive type (string, long, double, ..) ?? 
889                /*if (MetadataType.COMPOSITE == metadataDef.getType())
890                {
891                    throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path.");
892                }*/
893                _indexingModel.addIndexingField(new DefaultMetadataIndexingField(fieldName, metadataDef, metadataPath));
894            }
895            else
896            {
897                throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path.", fieldConf);
898            }
899        }
900    }
901
902    /**
903     * Configure the custom indexing fields.
904     * @param indexConf the indexing model configuration.
905     * @throws ConfigurationException if an error occurs.
906     */
907    @SuppressWarnings("unchecked")
908    protected void _configureCustomIndexingFields(Configuration indexConf) throws ConfigurationException
909    {
910        List<String> customFieldRoles = new ArrayList<>();
911        
912        Configuration[] customsConf = indexConf.getChildren("custom-field");
913        for (Configuration customConf : customsConf)
914        {
915            String className = customConf.getAttribute("class", null);
916            if (className == null)
917            {
918                throw new ConfigurationException("A custom index field defined in content type '" + this.getId() + "' does not specifiy a class.", customConf);
919            }
920            
921            try
922            {
923                Class<CustomIndexingField> customFieldClass = (Class<CustomIndexingField>) Class.forName(className);
924                String fieldRole = getId() + "-" + className;
925                _customFieldManager.addComponent("cms", null, getId() + "-" + className, customFieldClass, customConf);
926                
927                customFieldRoles.add(fieldRole);
928            }
929            catch (Exception e)
930            {
931                throw new ConfigurationException("Unable to instanciate custom indexing field for class: " + className, customConf, e);
932            }
933        }
934        
935        try
936        {
937            _customFieldManager.initialize();
938        }
939        catch (Exception e)
940        {
941            throw new ConfigurationException("Unable to initialize custom indexing field manager", e);
942        }
943        
944        for (String customFieldRole : customFieldRoles)
945        {
946            try
947            {
948                CustomIndexingField field = _customFieldManager.lookup(customFieldRole);
949                _indexingModel.addIndexingField(field);
950            }
951            catch (ComponentException e)
952            {
953                throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customFieldRole + "' for content type: " + this.getId(), e);
954            }
955        }
956    }
957    
958    /**
959     * Configure the custom metadata indexing fields.
960     * @param indexConf the indexing model configuration.
961     * @throws ConfigurationException if an error occurs.
962     */
963    @SuppressWarnings("unchecked")
964    protected void _configureCustomMetadataIndexingFields(Configuration indexConf) throws ConfigurationException
965    {
966        List<String> customMetadataFieldRoles = new ArrayList<>();
967        
968        int index = 0;
969        Configuration[] customMetaConfs = indexConf.getChildren("custom-metadata-field");
970        for (Configuration customMetaConf : customMetaConfs)
971        {
972            String className = customMetaConf.getAttribute("class", null);
973            if (className == null)
974            {
975                throw new ConfigurationException("A custom indexing field defined in content type '" + this.getId() + "' does not specifiy a class.", customMetaConf);
976            }
977            
978            try
979            {
980                DefaultConfiguration localConf = new DefaultConfiguration(customMetaConf, true);
981                DefaultConfiguration cTypeConf = new DefaultConfiguration("contentType");
982                cTypeConf.setAttribute("id", this.getId());
983                localConf.addChild(cTypeConf);
984                
985                // Use an index in the role to be able to use several times the same class in a content-type.
986                String fieldRole = getId() + "-" + className + "-" + index;
987                Class<CustomMetadataIndexingField> customMetaFieldClass = (Class<CustomMetadataIndexingField>) Class.forName(className);
988                _customMetadataIndexingFieldManager.addComponent("cms", null, fieldRole, customMetaFieldClass, localConf);
989                
990                customMetadataFieldRoles.add(fieldRole);
991                
992                index++;
993            }
994            catch (Exception e)
995            {
996                throw new ConfigurationException("Unable to instanciate custom metadata indexing field for class: " + className, customMetaConf, e);
997            }
998        }
999        
1000        try
1001        {
1002            _customMetadataIndexingFieldManager.initialize();
1003        }
1004        catch (Exception e)
1005        {
1006            throw new ConfigurationException("Unable to initialize custom metadata indexing field manager", e);
1007        }
1008        
1009        for (String customMetaFieldRole : customMetadataFieldRoles)
1010        {
1011            try
1012            {
1013                CustomMetadataIndexingField field = _customMetadataIndexingFieldManager.lookup(customMetaFieldRole);
1014                _indexingModel.addIndexingField(field);
1015            }
1016            catch (ComponentException e)
1017            {
1018                throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customMetaFieldRole + "' for content type: " + this.getId(), e);
1019            }
1020        }
1021    }
1022    
1023    /**
1024     * Parse the tags
1025     * @param configuration the configuration to use
1026     * @return the tags
1027     * @throws ConfigurationException if the configuration is not valid.
1028     */
1029    protected Set<String> _parseTags (Configuration configuration)  throws ConfigurationException
1030    {
1031        Set<String> tags = new HashSet<>();
1032        
1033        Configuration[] children = configuration.getChildren("tag");
1034        for (Configuration tagConfig : children)
1035        {
1036            tags.add(tagConfig.getValue());
1037        }
1038        
1039        return tags;
1040    }
1041    
1042    @Override
1043    public void postInitialize() throws Exception
1044    {
1045        _checkContentMetadatas();
1046        _checkContentMutualReferences();
1047        
1048        _computeIndexingModelReferences();
1049    }
1050    
1051    /**
1052     * Check that content and sub-content metadatas reference a valid content-type.
1053     * @throws ConfigurationException if a content (or sub-content) metadata references an invalid or non-existing content type.
1054     */
1055    protected void _checkContentMetadatas() throws ConfigurationException
1056    {
1057        for (MetadataDefinition metadataDef : _metadata.values())
1058        {
1059            _checkContentMetadata(metadataDef, metadataDef.getName());
1060        }
1061    }
1062    
1063    /**
1064     * Recursively check that content and sub-content metadatas reference a valid content-type.
1065     * @param metadataDef the metadata definition.
1066     * @param metadataPath the metadata path.
1067     * @throws ConfigurationException if a content (or sub-content) metadata references an invalid or non-existing content type.
1068     */
1069    protected void _checkContentMetadata(MetadataDefinition metadataDef, String metadataPath) throws ConfigurationException
1070    {
1071        if (metadataDef.getType() == MetadataType.CONTENT || metadataDef.getType() == MetadataType.SUB_CONTENT)
1072        {
1073            String cTypeId = metadataDef.getContentType();
1074            
1075            if (StringUtils.isNotBlank(cTypeId) && !_cTypeEP.hasExtension(cTypeId))
1076            {
1077                throw new ConfigurationException("The content metadata of path " + metadataPath + " in content type " + getId() + " references a non-existing content-type: '" + cTypeId + "'");
1078            }
1079        }
1080        
1081        // Check sub-metadatas.
1082        for (String subMetaName : metadataDef.getMetadataNames())
1083        {
1084            MetadataDefinition subMetaDef = metadataDef.getMetadataDefinition(subMetaName);
1085            String subMetaPath = metadataPath + "/" + subMetaName;
1086            
1087            _checkContentMetadata(subMetaDef, subMetaPath);
1088        }
1089    }
1090    
1091    /**
1092     * Check content type mutual reference declarations.
1093     * @throws ConfigurationException if there is a problem with mutual reference declarations.
1094     */
1095    protected void _checkContentMutualReferences() throws ConfigurationException
1096    {
1097        for (MetadataDefinition metadataDef : _metadata.values())
1098        {
1099            _checkContentMutualReferences(metadataDef, metadataDef.getName(), 0);
1100        }
1101    }
1102    
1103    /**
1104     * Recursively check a content type mutual reference declarations.
1105     * @param metadataDef the metadata definition.
1106     * @param metadataPath the metadata path.
1107     * @param repeaterLevel the current nested level of repeaters, 0 if the current metadata isn't in a repeater.
1108     * @throws ConfigurationException if there is a problem with mutual reference declarations.
1109     */
1110    protected void _checkContentMutualReferences(MetadataDefinition metadataDef, String metadataPath, int repeaterLevel) throws ConfigurationException
1111    {
1112        if (metadataDef.getType() == MetadataType.CONTENT)
1113        {
1114            String cTypeId = metadataDef.getContentType();
1115            String invertRelationPath = metadataDef.getInvertRelationPath();
1116            
1117            if (StringUtils.isNotEmpty(invertRelationPath))
1118            {
1119                ContentType cType = _cTypeEP.getExtension(cTypeId);
1120                MetadataDefinition invertMetadataDef = cType.getMetadataDefinitionByPath(invertRelationPath);
1121                
1122                if (repeaterLevel > 0 && metadataDef.isMultiple())
1123                {
1124                    throw new ConfigurationException("The metadata at path '" + metadataPath + "' in content type " + getId() + " is declared as a mutual relationship, it can't be multiple AND in a repeater.");
1125                }
1126                else if (repeaterLevel >= 2)
1127                {
1128                    throw new ConfigurationException("The metadata at path '" + metadataPath + "' in content type " + getId() + " is declared as a mutual relationship, it can't be in two levels of repeaters.");
1129                }
1130                
1131                // Ensure the metadata presence.
1132                if (invertMetadataDef == null)
1133                {
1134                    throw new ConfigurationException("Mutual relationship: the metadata at path '" + invertRelationPath + "' doesn't exist for type " + cTypeId);
1135                }
1136                
1137                // Ensure that the referenced metadata is a Content.
1138                if (invertMetadataDef.getType() != MetadataType.CONTENT)
1139                {
1140                    throw new ConfigurationException("Mutual relationship: the metadata at path '" + invertRelationPath + "' of type " + cTypeId + " is not of type Content.");
1141                }
1142                
1143                String invertCTypeId = invertMetadataDef.getContentType();
1144                String invertPath = invertMetadataDef.getInvertRelationPath();
1145                
1146                // Ensure that the referenced metadata's content type is compatible with the current type.
1147                if (!_cTypeEP.isSameOrDescendant(getId(), invertCTypeId))
1148                {
1149                    throw new ConfigurationException("Mutual relationship: the metadata at path " + invertRelationPath + " of type " + cTypeId + " references an incompatible type: " + StringUtils.defaultString(invertCTypeId, "<null>"));
1150                }
1151                
1152                // Ensure that the referenced metadata references this metadata.
1153                if (!metadataPath.equals(invertPath))
1154                {
1155                    throw new ConfigurationException("Mutual relationship: the metadata at path " + metadataPath + " of type " + getId() + " references the metadata '" + invertRelationPath + "' of type " + cTypeId + " but the latter does not reference it back.");
1156                }
1157            }
1158        }
1159        
1160        // We are in a repeater if we were already in a repeater at the previous level, or if the current metadata definition is a repeater.
1161        int newRepeaterLevel = (metadataDef instanceof RepeaterDefinition) ? repeaterLevel + 1 : repeaterLevel;
1162        
1163        // Check sub-metadatas.
1164        for (String subMetaName : metadataDef.getMetadataNames())
1165        {
1166            MetadataDefinition subMetaDef = metadataDef.getMetadataDefinition(subMetaName);
1167            String subMetaPath = metadataPath + ContentConstants.METADATA_PATH_SEPARATOR + subMetaName;
1168            
1169            _checkContentMutualReferences(subMetaDef, subMetaPath, newRepeaterLevel);
1170        }
1171    }
1172    
1173    /**
1174     * Browse the indexing model and compute indexing field references.
1175     */
1176    protected void _computeIndexingModelReferences()
1177    {
1178        // Impacted ContentType -> local IndexingField name -> path to impacted content.
1179        Map<String, Map<String, List<String>>> references = new HashMap<>();
1180        
1181        for (IndexingField field : _indexingModel.getFields())
1182        {
1183            if (field instanceof MetadataIndexingField)
1184            {
1185                String metadataPath = ((MetadataIndexingField) field).getMetadataPath();
1186                
1187                List<MetadataDefinition> definitions = getIndexingFieldDefinitions(this, metadataPath);
1188                
1189                List<String> joinPaths = new ArrayList<>();
1190                boolean localContentType = true;
1191                StringBuilder currentContentPath = new StringBuilder();
1192                for (MetadataDefinition definition : definitions)
1193                {
1194                    if (currentContentPath.length() > 0)
1195                    {
1196                        currentContentPath.append(ContentConstants.METADATA_PATH_SEPARATOR);
1197                    }
1198                    currentContentPath.append(definition.getName());
1199                    
1200                    if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT)
1201                    {
1202                        if (!localContentType)
1203                        {
1204                            joinPaths.add(currentContentPath.toString());
1205                            currentContentPath.setLength(0);
1206                            
1207                            String cTypeId = definition.getContentType();
1208                            
1209                            Map<String, List<String>> cTypeRefs = null;
1210                            if (references.containsKey(cTypeId))
1211                            {
1212                                cTypeRefs = references.get(cTypeId);
1213                            }
1214                            else
1215                            {
1216                                cTypeRefs = new HashMap<>();
1217                                references.put(cTypeId, cTypeRefs);
1218                            }
1219                            
1220                            cTypeRefs.put(field.getName(), new ArrayList<>(joinPaths));
1221                        }
1222                        
1223                        localContentType = false;
1224                    }
1225                }
1226            }
1227        }
1228        
1229        _indexingModel.setReferences(references);
1230    }
1231    
1232    /**
1233     * Get the list of metadata definitions "traversed" from the initial content type to the given metadata.
1234     * @param initialContentType the initial content type.
1235     * @param metadataPath the compound metadata path.
1236     * @return the list of metadata definitions.
1237     */
1238    protected List<MetadataDefinition> getIndexingFieldDefinitions(ContentType initialContentType, String metadataPath)
1239    {
1240        List<MetadataDefinition> definitions = new ArrayList<>();
1241        
1242        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1243        
1244        if (pathSegments.length > 0)
1245        {
1246            IndexingModel indexingModel = _contentTypesHelper.getIndexingModel(new String[] {initialContentType.getId()}, new String[0]);
1247            
1248            IndexingField refField = indexingModel.getField(pathSegments[0]);
1249            
1250            MetadataDefinition metadataDef = null;
1251            if (refField != null && refField instanceof CustomMetadataIndexingField)
1252            {
1253                metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition();
1254            }
1255            else
1256            {
1257                metadataDef = initialContentType.getMetadataDefinition(pathSegments[0]);
1258            }
1259            
1260            if (metadataDef != null)
1261            {
1262                definitions.add(metadataDef);
1263            }
1264            
1265            for (int i = 1; i < pathSegments.length && metadataDef != null; i++)
1266            {
1267                if (metadataDef.getType() == MetadataType.CONTENT || metadataDef.getType() == MetadataType.SUB_CONTENT)
1268                {
1269                    String refCTypeId = metadataDef.getContentType();
1270                    if (refCTypeId != null && _cTypeEP.hasExtension(refCTypeId))
1271                    {
1272                        ContentType refCType = _cTypeEP.getExtension(refCTypeId);
1273                        
1274                        List<MetadataDefinition> followingDefs = getIndexingFieldDefinitions(refCType, StringUtils.join(pathSegments, '/', i, pathSegments.length));
1275                        definitions.addAll(followingDefs);
1276                        
1277                        return definitions;
1278                    }
1279                }
1280                else
1281                {
1282                    refField = indexingModel.getField(pathSegments[i]);
1283                    if (refField != null && refField instanceof CustomMetadataIndexingField)
1284                    {
1285                        metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition();
1286                    }
1287                    else
1288                    {
1289                        metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
1290                    }
1291                    definitions.add(metadataDef);
1292                }
1293            }
1294        }
1295        
1296        return definitions;
1297    }
1298    
1299    @Override
1300    public List<ContentValidator> getGlobalValidators()
1301    {
1302        return Collections.unmodifiableList(_globalValidators);
1303    }
1304    
1305    @Override
1306    public RichTextUpdater getRichTextUpdater()
1307    {
1308        return _richTextUpdater;
1309    }
1310    
1311    @Override
1312    public Set<String> getMetadataNames()
1313    {
1314        return Collections.unmodifiableSet(_metadata.keySet());
1315    }
1316
1317    @Override
1318    public MetadataDefinition getMetadataDefinition(String metadataName)
1319    {
1320        return _metadata.get(metadataName);
1321    }
1322    
1323    @Override
1324    public boolean hasMetadataDefinition(String metadataName)
1325    {
1326        return _metadata.containsKey(metadataName);
1327    }
1328    
1329    @Override
1330    public MetadataDefinition getMetadataDefinitionByPath(String metadataPath)
1331    {
1332        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
1333        
1334        if (pathSegments.length == 0)
1335        {
1336            return null;
1337        }
1338        
1339        MetadataDefinition metadataDef = _metadata.get(pathSegments[0]);
1340        
1341        for (int i = 1;  i < pathSegments.length && metadataDef != null; i++)
1342        {
1343            metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
1344        }
1345        
1346        return metadataDef;
1347    }
1348    
1349    @Override
1350    public IndexingModel getIndexingModel()
1351    {
1352        return _indexingModel;
1353    }
1354    
1355    @Override
1356    public boolean canRead(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException
1357    {
1358        Restrictions restrictions = _getRestrictionsForPath(metadataDef);
1359        
1360        if (restrictions == null)
1361        {
1362            return true;
1363        }
1364            
1365        if (restrictions._cannotRead)
1366        {
1367            return false;
1368        }
1369        
1370        if (content == null)
1371        {
1372            // Unable to check right (content is not yet created), assume user has right 
1373            return true;
1374        }
1375        
1376        boolean hasRights = _hasRights(content, restrictions._readRights);
1377        
1378        if (!hasRights)
1379        {
1380            return false;
1381        }
1382        
1383        if (content instanceof WorkflowAwareContent)
1384        {
1385            hasRights = _isInWorkflowStep((WorkflowAwareContent) content, restrictions._readWfSteps);
1386            
1387            if (!hasRights)
1388            {
1389                return false;
1390            }
1391        }
1392        
1393        String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR);
1394        if (pathSegments.length > 1)
1395        {
1396            // Check read access on parent metadata definition
1397            String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR));
1398            MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content);
1399            return canRead(content, parentMetadataDef);
1400        }
1401        
1402        return true;
1403    }
1404
1405    @Override
1406    public boolean canWrite(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException
1407    {
1408        Restrictions restrictions = _getRestrictionsForPath(metadataDef);
1409        
1410        if (restrictions == null)
1411        {
1412            return true;
1413        }
1414    
1415        if (restrictions._cannotWrite)
1416        {
1417            return false;
1418        }
1419     
1420        if (content == null)
1421        {
1422            // Unable to check right (content is not yet created), assume user has right 
1423            return true;
1424        }
1425        
1426        boolean hasRights = _hasRights(content, restrictions._writeRights);
1427        
1428        if (!hasRights)
1429        {
1430            return false;
1431        }
1432        
1433        if (content instanceof WorkflowAwareContent)
1434        {
1435            hasRights = _isInWorkflowStep((WorkflowAwareContent) content, restrictions._writeWfSteps);
1436            
1437            if (!hasRights)
1438            {
1439                return false;
1440            }
1441        }
1442        
1443        String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR);
1444        if (pathSegments.length > 1)
1445        {
1446            // Check write access on parent metadata definition
1447            String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR));
1448            MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content);
1449            return canWrite(content, parentMetadataDef);
1450        }
1451
1452        return canRead(content, metadataDef);
1453    }
1454    
1455    @Override
1456    public Set<String> getTags()
1457    {
1458        return Collections.unmodifiableSet(_tags);
1459    }
1460    
1461    @Override
1462    public boolean hasTag(String tagName)
1463    {
1464        return _tags.contains(tagName);
1465    }
1466    
1467    @Override
1468    public boolean isPrivate()
1469    {
1470        return hasTag(TAG_PRIVATE);
1471    }
1472    
1473    @Override
1474    public boolean isAbstract()
1475    {
1476        return _abstract;
1477    }
1478    
1479    @Override
1480    public boolean isSimple()
1481    {
1482        return _isSimple;
1483    }
1484
1485    @Override
1486    public boolean isReferenceTable()
1487    {
1488        return hasTag(TAG_REFERENCE_TABLE) || hasTag(TAG_RENDERABLE_FERENCE_TABLE);
1489    }
1490    
1491    @Override
1492    public boolean isMultilingual()
1493    {
1494        return _isMultilingual;
1495    }
1496    
1497    @Override
1498    public boolean isMixin()
1499    {
1500        return hasTag(TAG_MIXIN);
1501    }
1502    
1503    @Override
1504    public String getRight()
1505    {
1506        return _right;
1507    }
1508    
1509    @Override
1510    public void saxContentTypeAdditionalData(org.xml.sax.ContentHandler contentHandler, Content content) throws AmetysRepositoryException, SAXException
1511    {
1512        // Nothing
1513    }
1514    
1515    @Override
1516    public Map<String, Object> getAdditionalData(Content content)
1517    {
1518        return new HashMap<>();
1519    }
1520    
1521    /**
1522     * Retrieves the restrictions for a given path.
1523     * @param metadataDef the metadata definition.
1524     * @return the restrictions or <code>null</code> if not found.
1525     */
1526    protected Restrictions _getRestrictionsForPath(MetadataDefinition metadataDef)
1527    {
1528        if (metadataDef != null && metadataDef instanceof RestrictedDefinition)
1529        {
1530            return ((RestrictedDefinition) metadataDef).getRestrictions();
1531        }
1532        
1533        // Not found
1534        return null;
1535    }
1536
1537    /**
1538     * Check if current user has the given rights.
1539     * @param rightLimitations the right limitations.
1540     * @param content the content.
1541     * @return <code>true</code> if it is on at least one step,
1542     *         <code>false</code> otherwise.
1543     */
1544    protected boolean _hasRights(Content content, Set<String> rightLimitations)
1545    {
1546        if (rightLimitations.isEmpty())
1547        {
1548            return true;
1549        }
1550
1551        UserIdentity user = _currentUserProvider.getUser();
1552
1553        for (String rightId : rightLimitations)
1554        {
1555            if (_rightManager.hasRight(user, rightId, content) == RightResult.RIGHT_ALLOW)
1556            {
1557                return true;
1558            }
1559        }
1560
1561        return false;
1562    }
1563
1564    /**
1565     * Check if the workflow of the content is in a given current step.
1566     * @param workflowLimitations the workflow limitations.
1567     * @param content the content.
1568     * @return <code>true</code> if it is on at least one step,
1569     *         <code>false</code> otherwise.
1570     * @throws AmetysRepositoryException if an error occurs.
1571     */
1572    protected boolean _isInWorkflowStep(WorkflowAwareContent content, Set<Integer> workflowLimitations) throws AmetysRepositoryException
1573    {
1574        if (workflowLimitations.isEmpty())
1575        {
1576            return true;
1577        }
1578        
1579        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
1580        
1581        List<Step> workflowCurrentSteps = workflow.getCurrentSteps(content.getWorkflowId());
1582        
1583        for (Step step : workflowCurrentSteps)
1584        {
1585            for (int stepId : workflowLimitations)
1586            {
1587                if (step.getStepId() == stepId)
1588                {
1589                    return true;
1590                }
1591            }
1592        }
1593
1594        // No match
1595        return false;
1596    }
1597    
1598    @Override
1599    public String toString()
1600    {
1601        return "'" + getId() + "'";
1602    }
1603    
1604    /**
1605     * Restrictions provided with a metadata definition.
1606     */
1607    protected static class Restrictions
1608    {
1609        /** Cannot read status. */
1610        protected boolean _cannotRead;
1611        /** Cannot write status. */
1612        protected boolean _cannotWrite;
1613        /** Read right ids. */
1614        protected Set<String> _readRights = new HashSet<>();
1615        /** Write right ids. */
1616        protected Set<Integer> _readWfSteps = new HashSet<>();
1617        /** Read workflow step ids. */
1618        protected Set<String> _writeRights = new HashSet<>();
1619        /** Write workflow step ids. */
1620        protected Set<Integer> _writeWfSteps = new HashSet<>();
1621    }
1622    
1623    /**
1624     * Restricted definition.
1625     */
1626    protected interface RestrictedDefinition
1627    {
1628        /**
1629         * Provides the restrictions.
1630         * @return the restrictions.
1631         */
1632        Restrictions getRestrictions();
1633    }
1634    
1635    /**
1636     * Definition with semantic annotations
1637     */
1638    protected interface AnnotableDefinition
1639    {
1640        /**
1641         * Provides the semantic annotations
1642         * @return the semantic annotations
1643         */
1644        List<SemanticAnnotation> getSemanticAnnotations();
1645
1646        /**
1647         * Set the semantic annotations
1648         * @param annotations the semantic annotations to set
1649         */
1650        void setSemanticAnnotations(List<SemanticAnnotation> annotations);
1651    }
1652    
1653    /**
1654     * Internal {@link MetadataDefinition} storage contains instances of this class.
1655     */
1656    protected static class RestrictedMetadataDefinition extends MetadataDefinition implements RestrictedDefinition
1657    {
1658        /** Restrictions. */
1659        protected Restrictions _restrictions = new Restrictions();
1660        
1661        @Override
1662        public Restrictions getRestrictions()
1663        {
1664            return _restrictions;
1665        }
1666    }
1667    
1668    /**
1669     * Internal {@link MetadataDefinition} storage contains instances of this class.
1670     */
1671    protected static class RestrictedRichTextDefinition extends RichTextMetadataDefinition  implements RestrictedDefinition
1672    {
1673        /** Restrictions. */
1674        protected Restrictions _restrictions = new Restrictions();
1675        
1676        @Override
1677        public Restrictions getRestrictions()
1678        {
1679            return _restrictions;
1680        }
1681    }
1682    
1683    /**
1684     * Internal {@link RepeaterDefinition} storage contains instances of this class.
1685     */
1686    protected static class RestrictedRepeaterDefinition extends RepeaterDefinition implements RestrictedDefinition
1687    {
1688        /** Restrictions. */
1689        protected Restrictions _restrictions = new Restrictions();
1690        
1691        @Override
1692        public Restrictions getRestrictions()
1693        {
1694            return _restrictions;
1695        }
1696    }
1697    
1698    /**
1699     * {@link RestrictedMetadataDefinition} and {@link RestrictedRepeaterDefinition} parser.
1700     */
1701    protected class MetadataAndRepeaterDefinitionParser extends AbstractParameterParser<MetadataDefinition, MetadataType>
1702    {
1703        /** Parent prefix. */
1704        protected String _parentPrefix = "";
1705        
1706        /**
1707         * Creates an {@link MetadataAndRepeaterDefinitionParser}.
1708         * @param enumeratorManager the enumerator component manager.
1709         * @param validatorManager the validator component manager.
1710         */
1711        public MetadataAndRepeaterDefinitionParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
1712        {
1713            super(enumeratorManager, validatorManager);
1714        }
1715        
1716        @Override
1717        protected MetadataDefinition _createParameter(Configuration metadataConfiguration)  throws ConfigurationException
1718        {
1719            String defName = metadataConfiguration.getName();                        
1720                        
1721            if (defName.equals("metadata"))
1722            {
1723                String defType = metadataConfiguration.getAttribute("type");
1724                if (defType.equals("rich-text"))
1725                {
1726                    return new RestrictedRichTextDefinition();
1727                } 
1728                else 
1729                {
1730                    return new RestrictedMetadataDefinition();                    
1731                }
1732                
1733            }
1734            else if (defName.equals("repeater"))
1735            {
1736                return new RestrictedRepeaterDefinition();
1737            }
1738
1739            throw new ConfigurationException("Unsupported metadata or repeater configuration", metadataConfiguration);
1740        }
1741        
1742        @Override
1743        protected String _parseId(Configuration metadataConfiguration) throws ConfigurationException
1744        {
1745            String metadataName = metadataConfiguration.getAttribute("name");
1746            
1747            if (!metadataName.matches("^[a-zA-Z]((?!__)[a-zA-Z0-9_-])*$"))
1748            {
1749                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);
1750            }
1751            
1752            return _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName;
1753        }
1754        
1755        @Override
1756        protected MetadataType _parseType(Configuration metadataConfiguration) throws ConfigurationException
1757        {
1758            if (metadataConfiguration.getName().equals("repeater"))
1759            {
1760                // A repeater is a composite
1761                return MetadataType.COMPOSITE;
1762            }
1763            
1764            try
1765            {
1766                return MetadataType.valueOf(metadataConfiguration.getAttribute("type").toUpperCase().replaceAll("-", "_"));
1767            }
1768            catch (IllegalArgumentException e)
1769            {
1770                throw new ConfigurationException("Invalid type", metadataConfiguration, e);
1771            }
1772        }
1773        
1774        @Override
1775        protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException
1776        {
1777            // Override i18n parsing to use the default catalog (which can be application for automatic content types)
1778            return I18nizableText.parseI18nizableText(config.getChild(name), _getDefaultCatalogue());
1779        }
1780        
1781        @Override
1782        protected Object _parseDefaultValue(Configuration metadataConfiguration, MetadataDefinition metadataDef)
1783        {
1784            String value;
1785            
1786            Configuration childNode = metadataConfiguration.getChild("default-value", false);
1787            if (childNode == null)
1788            {
1789                value = null;
1790            }
1791            else
1792            {
1793                value = childNode.getValue("");
1794            }
1795            
1796            return value;
1797        }
1798        
1799        @Override
1800        protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration metadataConfiguration, String metadataId, MetadataDefinition metadataDefinition) throws ConfigurationException
1801        {
1802            super._additionalParsing(manager, pluginName, metadataConfiguration, metadataId, metadataDefinition);
1803            String metadataName = metadataConfiguration.getAttribute("name");
1804            
1805            metadataDefinition.setReferenceContentType(_id);
1806            metadataDefinition.setName(metadataName);
1807            metadataDefinition.setMultiple(metadataConfiguration.getAttributeAsBoolean("multiple", false));
1808            // Use default transformer (docbook)
1809            metadataDefinition.setRichTextTransformer(_richTextTransformer);
1810            // Use docbook outgoing consistency extractor
1811            metadataDefinition.setRichTextOutgoingReferencesExtractor(_richTextOutgoingReferencesExtractor);
1812            
1813            if (metadataDefinition instanceof RepeaterDefinition)
1814            {
1815                RepeaterDefinition repeaterDefinition = (RepeaterDefinition) metadataDefinition;
1816                
1817                _parseRepeaterDefinition(manager, pluginName, metadataConfiguration, repeaterDefinition);
1818            }
1819            
1820            if (metadataDefinition instanceof AnnotableDefinition)
1821            {
1822                AnnotableDefinition definitionWithSemAnnotations = (AnnotableDefinition) metadataDefinition;
1823                
1824                _parseDefinitionWithAnnotations(manager, pluginName, metadataConfiguration, definitionWithSemAnnotations);
1825            }
1826            
1827            _populateRestrictions(metadataConfiguration, ((RestrictedDefinition) metadataDefinition).getRestrictions());
1828            
1829            if (metadataDefinition.getType() == MetadataType.CONTENT || metadataDefinition.getType() == MetadataType.SUB_CONTENT)
1830            {
1831                // Content metadata: parse and set the content type restriction.
1832                String contentType = metadataConfiguration.getAttribute("contentType", null);
1833                if (StringUtils.isNotEmpty(contentType))
1834                {
1835                    metadataDefinition.setContentType(contentType);
1836                }
1837                
1838                if (metadataDefinition.getType() == MetadataType.CONTENT)
1839                {
1840                    _parseContentRelations(metadataConfiguration, metadataDefinition, contentType);
1841                }
1842            }
1843            else if (metadataDefinition.getType() == MetadataType.COMPOSITE)
1844            {
1845                for (Configuration childConfig : metadataConfiguration.getChildren())
1846                {
1847                    String childName = childConfig.getName();
1848                    
1849                    if (childName.equals("metadata") || childName.equals("repeater"))
1850                    {
1851                        String oldParentPrefix = _parentPrefix;
1852                        // Stack new parent prefix
1853                        _parentPrefix = _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName;
1854                        MetadataDefinition subMetaDef = parseParameter(manager, pluginName, childConfig);
1855                        // Restore parent prefix
1856                        _parentPrefix = oldParentPrefix;
1857                        
1858                        if (!metadataDefinition.addMetadata(subMetaDef))
1859                        {
1860                            throw new ConfigurationException("Metadata with name " + subMetaDef.getName() + " is already defined", childConfig);
1861                        }
1862                    }
1863                }
1864            }
1865        }
1866        
1867        /**
1868         * Parse content mutual relations.
1869         * @param metadataConfiguration the metadata configuration.
1870         * @param metadataDefinition the metadata definition to fill.
1871         * @param contentType the content type.
1872         */
1873        protected void _parseContentRelations(Configuration metadataConfiguration, MetadataDefinition metadataDefinition, String contentType)
1874        {
1875            String invert = metadataConfiguration.getAttribute("invert", null);
1876            if (StringUtils.isNotEmpty(invert))
1877            {
1878                metadataDefinition.setInvertRelationPath(invert);
1879                
1880                boolean forceInvert = metadataConfiguration.getAttributeAsBoolean("forceInvert", false);
1881                metadataDefinition.setForceInvert(forceInvert);
1882            }
1883        }
1884        
1885        /**
1886         * Parses the repeater definition. 
1887         * @param manager the service manager.
1888         * @param pluginName the plugin name declaring this parameter.
1889         * @param metadataConfiguration the metadata configuration to use.
1890         * @param repeaterDefinition the repeater definition.
1891         * @throws ConfigurationException if the configuration is not valid.
1892         */
1893        protected void _parseRepeaterDefinition(ServiceManager manager, String pluginName, Configuration metadataConfiguration, RepeaterDefinition repeaterDefinition) throws ConfigurationException
1894        {
1895            repeaterDefinition.setAddLabel(_parseI18nizableText(metadataConfiguration, pluginName, "add-label"));
1896            repeaterDefinition.setDeleteLabel(_parseI18nizableText(metadataConfiguration, pluginName, "del-label"));
1897            repeaterDefinition.setHeaderLabel(metadataConfiguration.getChild("header-label").getValue(null));
1898            repeaterDefinition.setInitialSize(metadataConfiguration.getAttributeAsInteger("initial-size", 0));
1899            repeaterDefinition.setMinSize(metadataConfiguration.getAttributeAsInteger("min-size", 0));
1900            repeaterDefinition.setMaxSize(metadataConfiguration.getAttributeAsInteger("max-size", -1));
1901        }
1902        
1903        /**
1904         * Parses the definition with semantic annotations. 
1905         * @param manager the service manager.
1906         * @param pluginName the plugin name declaring this parameter.
1907         * @param metadataConfiguration the metadata configuration to use.
1908         * @param annotableDefinition the metadata definition
1909         * @throws ConfigurationException if the configuration is not valid.
1910         */
1911        protected void _parseDefinitionWithAnnotations(ServiceManager manager, String pluginName, Configuration metadataConfiguration, AnnotableDefinition annotableDefinition) throws ConfigurationException
1912        {            
1913            Configuration annotationsConfiguration = metadataConfiguration.getChild("annotations");
1914            List<SemanticAnnotation> semAnnotations = _parseSemAnnotations(pluginName, annotationsConfiguration);
1915            annotableDefinition.setSemanticAnnotations(semAnnotations);
1916        }
1917        
1918        /**
1919         * Extract the list of the declared annotations
1920         * @param pluginName the plugin name declaring this parameter.
1921         * @param annotationsConfiguration the annotations configuration to use.
1922         * @return the list of the declared annotations
1923         * @throws ConfigurationException if the configuration is not valid.
1924         */
1925        protected List<SemanticAnnotation> _parseSemAnnotations(String pluginName, Configuration annotationsConfiguration)  throws ConfigurationException
1926        {
1927            List<SemanticAnnotation> annotations = new ArrayList<>();
1928            
1929            for (Configuration annotationConfig : annotationsConfiguration.getChildren("annotation"))
1930            {
1931                String id = annotationConfig.getAttribute("name");
1932                
1933                if (!_getAnnotationNamePattern().matcher(id).matches())
1934                {
1935                    throw new ConfigurationException("Invalid annonation name '" + id + "'. This value is not permitted: only [a-zA-Z][a-zA-Z0-9-_]* are allowed.");
1936                }
1937                
1938                I18nizableText label = _parseI18nizableText(annotationConfig, pluginName, "label");
1939                I18nizableText description = _parseI18nizableText(annotationConfig, pluginName, "description");
1940                annotations.add(new SemanticAnnotation(id, label, description));
1941            }
1942            
1943            return annotations;
1944        }
1945        
1946        /**
1947         * Get the annotation name pattern to test validity.
1948         * @return The annotation name pattern.
1949         */
1950        protected Pattern _getAnnotationNamePattern()
1951        {
1952            if (__annotationNamePattern == null)
1953            {
1954                // [a-zA-Z][a-zA-Z0-9_]*
1955                __annotationNamePattern = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE);
1956            }
1957
1958            return __annotationNamePattern;
1959        }
1960
1961        /**
1962         * Populates the restrictions into a metadata definition.
1963         * @param metadataConfiguration the metadata configuration to use.
1964         * @param restrictions the restrictions.
1965         * @throws ConfigurationException if the configuration is not valid.
1966         */
1967        protected void _populateRestrictions(Configuration metadataConfiguration, Restrictions restrictions) throws ConfigurationException
1968        {
1969            Configuration restrictToConf = metadataConfiguration.getChild("restrict-to", true);
1970
1971            _populateNegativeRestrictions(restrictToConf, restrictions);
1972            _populateRightRestrictions(restrictToConf, restrictions);
1973            _populateWorkflowRestrictions(restrictToConf, restrictions);
1974        }
1975
1976        /**
1977         * Populates the negative restrictions.
1978         * @param restrictionsConfig the restrictions configuration to use.
1979         * @param restrictions the restrictions.
1980         * @throws ConfigurationException if the configuration is not valid.
1981         */
1982        protected void _populateNegativeRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
1983        {
1984            for (Configuration noRightConfig : restrictionsConfig.getChildren("cannot"))
1985            {
1986                boolean isRead = _parseAccessType(noRightConfig);
1987                
1988                if (isRead)
1989                {
1990                    restrictions._cannotRead = true;
1991                }
1992                else
1993                {
1994                    restrictions._cannotWrite = true;
1995                }
1996            }
1997        }
1998
1999        /**
2000         * Parses type attribute from a configuration.
2001         * @param configuration the configuration.
2002         * @return <code>true</code> for <code>read</code> type,
2003         *         <code>false</code> for <code>write</code> type.
2004         * @throws ConfigurationException if the configuration is not valid.
2005         */
2006        protected boolean _parseAccessType(Configuration configuration) throws ConfigurationException
2007        {
2008            String type = configuration.getAttribute("read-write-direction");
2009            
2010            if ("read".equalsIgnoreCase(type))
2011            {
2012                return  true;
2013            }
2014            else if ("write".equalsIgnoreCase(type))
2015            {
2016                return false;
2017            }
2018            else
2019            {
2020                throw new ConfigurationException("Attribute 'type' must be 'read' or 'write'.", configuration);
2021            }
2022        }
2023
2024        /**
2025         * Populates the rights restrictions.
2026         * @param restrictionsConfig the restrictions configuration to use.
2027         * @param restrictions the restrictions.
2028         * @throws ConfigurationException if the configuration is not valid.
2029         */
2030        protected void _populateRightRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
2031        {
2032            for (Configuration rightConfig : restrictionsConfig.getChildren("right"))
2033            {
2034                String rightId = rightConfig.getAttribute("id", null);
2035                
2036                if (rightId == null)
2037                {
2038                    throw new ConfigurationException("Attribute 'id' is mandatory on 'right' element in a content tye configuration.", rightConfig);
2039                }
2040
2041                boolean isRead = _parseAccessType(rightConfig);
2042                
2043                if (isRead)
2044                {
2045                    restrictions._readRights.add(rightId);
2046                }
2047                else
2048                {
2049                    restrictions._writeRights.add(rightId);
2050                }
2051            }
2052        }
2053
2054        /**
2055         * Populates the workflows restrictions.
2056         * @param restrictionsConfig the restrictions configuration to use.
2057         * @param restrictions the restrictions.
2058         * @throws ConfigurationException if the configuration is not valid.
2059         */
2060        protected void _populateWorkflowRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
2061        {
2062            for (Configuration workflowConfig : restrictionsConfig.getChildren("workflow"))
2063            {
2064                String stepId = workflowConfig.getAttribute("step", null);
2065                int stepIdValue = -1;
2066                
2067                if (stepId != null)
2068                {
2069                    try
2070                    {
2071                        stepIdValue = Integer.valueOf(stepId);
2072                    }
2073                    catch (NumberFormatException e)
2074                    {
2075                        // Handled just below
2076                    }
2077                }
2078                
2079                if (stepIdValue == -1)
2080                {
2081                    throw new ConfigurationException("Attribute 'step' is mandatory and must be an integer on 'workflow' element in a content type configuration.", workflowConfig);
2082                }
2083
2084                boolean isRead = _parseAccessType(workflowConfig);
2085                
2086                if (isRead)
2087                {
2088                    restrictions._readWfSteps.add(stepIdValue);
2089                }
2090                else
2091                {
2092                    restrictions._writeWfSteps.add(stepIdValue);
2093                }
2094            }
2095        }
2096    }
2097    
2098    @Override
2099    public MetadataDefinition getParentMetadata()
2100    {
2101        return _parentMetadataName != null ? _metadata.get(_parentMetadataName) : null;
2102    }
2103}