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.Collection; 020import java.util.Collections; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.avalon.framework.configuration.Configuration; 025import org.apache.avalon.framework.configuration.ConfigurationException; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.cocoon.ProcessingException; 028import org.apache.commons.lang3.StringUtils; 029import org.apache.commons.lang3.tuple.ImmutablePair; 030import org.apache.commons.lang3.tuple.Pair; 031import org.xml.sax.ContentHandler; 032import org.xml.sax.SAXException; 033 034import org.ametys.cms.data.ContentValue; 035import org.ametys.cms.repository.ContentQueryHelper; 036import org.ametys.cms.repository.ContentTypeExpression; 037import org.ametys.cms.repository.ModifiableContent; 038import org.ametys.core.util.XMLUtils; 039import org.ametys.plugins.repository.AmetysObjectIterable; 040import org.ametys.plugins.repository.AmetysObjectIterator; 041import org.ametys.plugins.repository.AmetysObjectResolver; 042import org.ametys.plugins.repository.query.expression.AndExpression; 043import org.ametys.plugins.repository.query.expression.DoubleExpression; 044import org.ametys.plugins.repository.query.expression.Expression; 045import org.ametys.plugins.repository.query.expression.Expression.Operator; 046import org.ametys.plugins.repository.query.expression.LongExpression; 047import org.ametys.plugins.repository.query.expression.StringExpression; 048import org.ametys.runtime.model.DefinitionContext; 049import org.ametys.runtime.model.ElementDefinition; 050import org.ametys.runtime.model.ModelItem; 051import org.ametys.runtime.model.ModelItemAccessor; 052import org.ametys.runtime.model.type.ElementType; 053import org.ametys.runtime.model.type.ModelItemTypeConstants; 054 055/** 056 * Definition of content types attributes of type Content 057 */ 058public class ContentAttributeDefinition extends AttributeDefinition<ContentValue> implements ModelItemAccessor 059{ 060 /** attribute type for default values */ 061 public static final String ATTRIBUTE_DEFAULT_VALUE_TYPE = "attribute"; 062 063 private ContentTypesHelper _contentTypesHelper; 064 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 065 private String _contentTypeId; 066 private String _invertRelationPath; 067 private boolean _forceInvert; 068 069 private boolean _isDefaultValueAlreadyChecked; 070 071 /** 072 * Defintion's constructor 073 * @param contentTypeExtensionPoint the content type extension point 074 * @param contentAttributeTypeExtensionPoint the content attribute type extension point 075 */ 076 public ContentAttributeDefinition(ContentTypeExtensionPoint contentTypeExtensionPoint, ContentTypesHelper contentAttributeTypeExtensionPoint) 077 { 078 _contentTypeExtensionPoint = contentTypeExtensionPoint; 079 _contentTypesHelper = contentAttributeTypeExtensionPoint; 080 } 081 082 /** 083 * Get the attribute's content type identifier. 084 * @return the attribute's content type identifier. 085 */ 086 public String getContentTypeId() 087 { 088 return _contentTypeId; 089 } 090 091 /** 092 * Set the attribute's content type identifier. 093 * @param contentTypeId the content type identifier to set. 094 */ 095 public void setContentTypeId(String contentTypeId) 096 { 097 _contentTypeId = contentTypeId; 098 } 099 100 /** 101 * Checks the default value of this definition 102 * @throws ConfigurationException if there is a problem with default value declaration 103 */ 104 @SuppressWarnings("unchecked") 105 public void checkDefaultValue() throws ConfigurationException 106 { 107 if (!_isDefaultValueAlreadyChecked) 108 { 109 if (_getParsedDefaultValues() != null) 110 { 111 List<Pair<String, Object>> parsedDefaultValues = new ArrayList<>(); 112 for (Pair<String, Object> parsedDefaultValue : _getParsedDefaultValues()) 113 { 114 String defaultValueType = parsedDefaultValue.getLeft(); 115 if (ATTRIBUTE_DEFAULT_VALUE_TYPE.equals(defaultValueType)) 116 { 117 assert parsedDefaultValue.getRight() instanceof Pair; 118 Pair<String, Configuration> defaultValue = (Pair<String, Configuration>) parsedDefaultValue.getRight(); 119 String attributeName = defaultValue.getLeft(); 120 Configuration defaultValueConfiguration = defaultValue.getRight(); 121 122 Object parsedAttributeDefaultValue = _checkAndParseAttributeDefaultValue(attributeName, defaultValueConfiguration); 123 parsedDefaultValues.add(new ImmutablePair<>(defaultValueType, new ImmutablePair<>(attributeName, parsedAttributeDefaultValue))); 124 } 125 else 126 { 127 parsedDefaultValues.add(parsedDefaultValue); 128 } 129 } 130 131 setParsedDefaultValues(parsedDefaultValues); 132 } 133 134 _isDefaultValueAlreadyChecked = true; 135 } 136 } 137 138 private Object _checkAndParseAttributeDefaultValue(String attributeName, Configuration defaultValueConfiguration) throws ConfigurationException 139 { 140 if (StringUtils.isEmpty(_contentTypeId)) 141 { 142 throw new ConfigurationException("The type '" + ATTRIBUTE_DEFAULT_VALUE_TYPE + "' is not available for the default value of item '" + getPath() + " (content)': this item does not specify a content type.", defaultValueConfiguration); 143 } 144 else 145 { 146 ContentType contentType = _contentTypeExtensionPoint.getExtension(_contentTypeId); 147 if (contentType.hasModelItem(attributeName) && contentType.getModelItem(attributeName) instanceof ElementDefinition) 148 { 149 ElementDefinition attribute = (ElementDefinition) contentType.getModelItem(attributeName); 150 ElementType attributeType = attribute.getType(); 151 if (ModelItemTypeConstants.STRING_TYPE_ID.equals(attributeType.getId()) || ModelItemTypeConstants.LONG_TYPE_ID.equals(attributeType.getId()) || ModelItemTypeConstants.DOUBLE_TYPE_ID.equals(attributeType.getId())) 152 { 153 // Parse the value of the attribute that must 154 return attributeType.parseConfiguration(defaultValueConfiguration); 155 } 156 else 157 { 158 throw new ConfigurationException("The type '" + ATTRIBUTE_DEFAULT_VALUE_TYPE + "' supports attributes of type " + ModelItemTypeConstants.STRING_TYPE_ID + ", " + ModelItemTypeConstants.LONG_TYPE_ID + " and " + ModelItemTypeConstants.DOUBLE_TYPE_ID + ". The attribute named '" + attributeName + "' is a " + attributeType.getId() + ", so it is not supported.", defaultValueConfiguration); 159 } 160 } 161 else 162 { 163 throw new ConfigurationException("The content type '" + _contentTypeId + "' has no attribute with named '" + attributeName + "'", defaultValueConfiguration); 164 } 165 } 166 } 167 168 @Override 169 protected ContentValue _getDefaultValue(String defaultValueType, Object parsedDefaultValue) 170 { 171 if (ATTRIBUTE_DEFAULT_VALUE_TYPE.equals(defaultValueType)) 172 { 173 // Launch a repository query to get the corresponding content 174 175 // Create the expression on the content type 176 Expression contentTypeExpression = new ContentTypeExpression(Operator.EQ, _contentTypeId); 177 178 // Create the expression on the attribute, according to its type 179 @SuppressWarnings("unchecked") 180 Pair<String, Configuration> defaultValue = (Pair<String, Configuration>) parsedDefaultValue; 181 String attributeName = defaultValue.getLeft(); 182 Object value = defaultValue.getRight(); 183 184 ContentType contentType = _contentTypeExtensionPoint.getExtension(_contentTypeId); 185 ElementType attributeType = ((ElementDefinition) contentType.getModelItem(attributeName)).getType(); 186 187 Expression attributeExpression; 188 if (ModelItemTypeConstants.LONG_TYPE_ID.equals(attributeType.getId())) 189 { 190 attributeExpression = new LongExpression(attributeName, Operator.EQ, (Long) value); 191 } 192 else if (ModelItemTypeConstants.DOUBLE_TYPE_ID.equals(attributeType.getId())) 193 { 194 attributeExpression = new DoubleExpression(attributeName, Operator.EQ, (Double) value); 195 } 196 else 197 { 198 attributeExpression = new StringExpression(attributeName, Operator.EQ, (String) value); 199 } 200 201 // Launch the repository query 202 Expression expression = new AndExpression(contentTypeExpression, attributeExpression); 203 String xPathQuery = ContentQueryHelper.getContentXPathQuery(expression); 204 205 try 206 { 207 AmetysObjectResolver resolver = (AmetysObjectResolver) __serviceManager.lookup(AmetysObjectResolver.ROLE); 208 AmetysObjectIterable<ModifiableContent> contents = resolver.query(xPathQuery); 209 AmetysObjectIterator<ModifiableContent> iterator = contents.iterator(); 210 211 return iterator.hasNext() ? new ContentValue(iterator.next()) : null; 212 } 213 catch (ServiceException e) 214 { 215 throw new RuntimeException("Unable to lookup after the ametys object resolver", e); 216 } 217 } 218 else 219 { 220 return super._getDefaultValue(defaultValueType, parsedDefaultValue); 221 } 222 } 223 224 /** 225 * Get the attribute's mutual relationship path. 226 * @return the attribute's mutual relationship path. 227 */ 228 public String getInvertRelationPath() 229 { 230 return _invertRelationPath; 231 } 232 233 /** 234 * Set the attribute's mutual relationship path. 235 * @param invertRelationPath the attribute's mutual relationship path, separated by slashes. 236 */ 237 public void setInvertRelationPath(String invertRelationPath) 238 { 239 _invertRelationPath = invertRelationPath; 240 } 241 242 /** 243 * Force mutual relationship regardless of user's rights (only applicable for an attribute with invert relation path). 244 * @param force true to force mutual relationship regardless of user's rights 245 */ 246 public void setForceInvert (boolean force) 247 { 248 _forceInvert = force; 249 } 250 251 /** 252 * Returns true if mutual relationship should be set regardless of user's rights 253 * @return true if mutual relationship should be set regardless of user's rights 254 */ 255 public boolean getForceInvert () 256 { 257 return _forceInvert; 258 } 259 260 public Collection< ? extends ModelItem> getModelItems() 261 { 262 if (_contentTypeId != null && _contentTypeExtensionPoint.hasExtension(_contentTypeId)) 263 { 264 ContentType contentType = _contentTypeExtensionPoint.getExtension(_contentTypeId); 265 return contentType.getModelItems(); 266 } 267 else 268 { 269 return Collections.singleton(_contentTypesHelper.getTitleAttributeDefinition()); 270 } 271 } 272 273 @Override 274 protected Map<String, Object> _toJSON(DefinitionContext context) throws ProcessingException 275 { 276 Map<String, Object> result = super._toJSON(context); 277 result.put("contentType", _contentTypeId); 278 result.put("canCreate", _contentTypesHelper.hasRight(_contentTypeId)); 279 return result; 280 } 281 282 @Override 283 public void toSAX(ContentHandler contentHandler, DefinitionContext context) throws SAXException 284 { 285 super.toSAX(contentHandler, context); 286 XMLUtils.createElementIfNotNull(contentHandler, "contentType", _contentTypeId); 287 } 288}