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}