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}