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