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