001/*
002 *  Copyright 2018 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.util.ArrayList;
019import java.util.List;
020import java.util.regex.Pattern;
021
022import org.apache.avalon.framework.configuration.Configuration;
023import org.apache.avalon.framework.configuration.ConfigurationException;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.commons.lang3.StringUtils;
027
028import org.ametys.cms.contenttype.DefaultContentType.AnnotableDefinition;
029import org.ametys.cms.data.type.ModelItemTypeConstants;
030import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper;
031import org.ametys.cms.repository.ContentAttributeTypeExtensionPoint;
032import org.ametys.runtime.i18n.I18nizableText;
033import org.ametys.runtime.model.ElementDefinitionParser;
034import org.ametys.runtime.model.Enumerator;
035import org.ametys.runtime.model.Model;
036import org.ametys.runtime.model.ModelItemGroup;
037import org.ametys.runtime.model.type.ModelItemType;
038import org.ametys.runtime.parameter.Validator;
039import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
040
041/**
042 * This class parses the content attributes definition
043 */
044public class ContentAttributeDefinitionParser extends ElementDefinitionParser
045{
046    /** Pattern for annotation names */
047    protected static Pattern __annotationNamePattern;
048    
049    /** the content type extension point */
050    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
051    
052    /** the content attribute type extension point */
053    protected ContentAttributeTypeExtensionPoint _contentAttributeTypeExtensionPoint;
054    
055    /**
056     * Creates a content attribute definition parser.
057     * @param contentAttributeTypeExtensionPoint the extension point to use to get available element types
058     * @param enumeratorManager the enumerator component manager.
059     * @param validatorManager the validator component manager.
060     */
061    public ContentAttributeDefinitionParser(ContentAttributeTypeExtensionPoint contentAttributeTypeExtensionPoint, ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
062    {
063        super(contentAttributeTypeExtensionPoint, enumeratorManager, validatorManager);
064    }
065
066    @Override
067    public AttributeDefinition parse(ServiceManager serviceManager, String pluginName, Configuration definitionConfig, Model model, ModelItemGroup parent) throws ConfigurationException
068    {
069        try
070        {
071            _contentAttributeTypeExtensionPoint = (ContentAttributeTypeExtensionPoint) serviceManager.lookup(ContentAttributeTypeExtensionPoint.ROLE);
072            _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
073        }
074        catch (ServiceException e)
075        {
076            throw new ConfigurationException("Unable to parse the attribute '" + _parseName(definitionConfig) + "' in model'" + model.getId() + "'.", definitionConfig, e);
077        }
078        
079        AttributeDefinition definition = (AttributeDefinition) super.parse(serviceManager, pluginName, definitionConfig, model, parent);
080        
081        try
082        {
083            ContentRestrictedModelItemHelper restrictedModelItemHelper = (ContentRestrictedModelItemHelper) serviceManager.lookup(ContentRestrictedModelItemHelper.ROLE);
084            definition.setRestrictions(restrictedModelItemHelper._parseRestrictions(definitionConfig));
085        }
086        catch (ServiceException e)
087        {
088            throw new ConfigurationException("Unable to resolve restrictions on the element '" + definition.getName() + "'.", e);
089        }
090        
091        if (definition instanceof AnnotableDefinition)
092        {
093            ((AnnotableDefinition) definition).setSemanticAnnotations(_parseDefinitionWithAnnotations(pluginName, definitionConfig));
094        }
095        
096        if (definition instanceof ContentAttributeDefinition)
097        {
098            ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) definition;
099            contentAttributeDefinition.setContentTypeId(_parseContentTypeId(definitionConfig));
100            
101            String invertRelationPath = _parseInvertRelationPath(definitionConfig);
102            if (StringUtils.isNotEmpty(invertRelationPath))
103            {
104                contentAttributeDefinition.setInvertRelationPath(invertRelationPath);
105                contentAttributeDefinition.setForceInvert(_parseForceInvert(definitionConfig));
106            }
107        }
108        
109        return definition;
110    }
111
112    @Override
113    protected AttributeDefinition _createModelItem(Configuration definitionConfig) throws ConfigurationException
114    {
115        ModelItemType type = _parseType(definitionConfig);
116        if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(type.getId()))
117        {
118            return new ContentAttributeDefinition(_contentTypeExtensionPoint, _contentAttributeTypeExtensionPoint);
119        }
120        else if (ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(type.getId()))
121        {
122            return new RichTextAttributeDefinition<>();
123        }
124        else
125        {
126            return new AttributeDefinition<>();            
127        }
128    }
129    
130    @Override
131    protected String _parseName(Configuration itemConfig) throws ConfigurationException
132    {
133        String itemName = itemConfig.getAttribute(_getNameConfigurationAttribute());
134        
135        if (!itemName.matches("^[a-zA-Z]((?!__)[a-zA-Z0-9_-])*$"))
136        {
137            throw new ConfigurationException("Invalid model item name: " + itemName + ". The item name must start with a letter and must contain only letters, digits, underscore or dash characters.", itemConfig);
138        }
139        
140        return itemName;
141    }
142    
143    /**
144     * Parses the semantic annotations of the model item
145     * @param pluginName the name of the plugin name declaring this item.
146     * @param itemConfig the model item configuration to use.
147     * @return the list of the declared annotations.
148     * @throws ConfigurationException if the configuration is not valid.
149     */
150    protected List<SemanticAnnotation> _parseDefinitionWithAnnotations(String pluginName, Configuration itemConfig) throws ConfigurationException
151    {            
152        Configuration annotationsConfiguration = itemConfig.getChild("annotations");
153        List<SemanticAnnotation> annotations = new ArrayList<>();
154        
155        for (Configuration annotationConfig : annotationsConfiguration.getChildren("annotation"))
156        {
157            String id = annotationConfig.getAttribute("name");
158            
159            if (!_getAnnotationNamePattern().matcher(id).matches())
160            {
161                throw new ConfigurationException("Invalid annonation name '" + id + "'. This value is not permitted: only [a-zA-Z][a-zA-Z0-9-_]* are allowed.");
162            }
163            
164            I18nizableText label = _parseI18nizableText(annotationConfig, pluginName, "label");
165            I18nizableText description = _parseI18nizableText(annotationConfig, pluginName, "description");
166            annotations.add(new SemanticAnnotation(id, label, description));
167        }
168        
169        return annotations;
170    }
171    
172    /**
173     * Get the annotation name pattern to test validity.
174     * @return The annotation name pattern.
175     */
176    protected Pattern _getAnnotationNamePattern()
177    {
178        if (__annotationNamePattern == null)
179        {
180            // [a-zA-Z][a-zA-Z0-9_]*
181            __annotationNamePattern = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE);
182        }
183
184        return __annotationNamePattern;
185    }
186    
187    /**
188     * Parses the content type identifier attribute.
189     * @param itemConfig the item configuration to use.
190     * @return the identifier of the content type or <code>null</code> if none defined.
191     * @throws ConfigurationException if the defined content type des not exist
192     */
193    protected String _parseContentTypeId(Configuration itemConfig) throws ConfigurationException
194    {
195        return itemConfig.getAttribute("contentType", null);
196    }
197    
198    /**
199     * Parses the invert relation path attribute.
200     * @param itemConfig the item configuration to use.
201     * @return the invert relation path or <code>null</code> if none defined.
202     */
203    protected String _parseInvertRelationPath(Configuration itemConfig)
204    {
205        return itemConfig.getAttribute("invert", null);
206    }
207    
208    /**
209     * Parses the force invert attribute.
210     * @param itemConfig the item configuration to use.
211     * @return <code>true</code> if mutual relationship of the item should be set regardless of user's rights, <code>false</code> otherwise.
212     */
213    protected boolean _parseForceInvert(Configuration itemConfig)
214    {
215        return itemConfig.getAttributeAsBoolean("forceInvert", false);
216    }
217}