001/*
002 *  Copyright 2013 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.Collections;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.acting.ServiceableAction;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Redirector;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.environment.SourceResolver;
033import org.apache.commons.lang.StringUtils;
034
035import org.ametys.cms.content.RootContentHelper;
036import org.ametys.cms.repository.Content;
037import org.ametys.core.cocoon.JSonReader;
038import org.ametys.core.right.RightManager;
039import org.ametys.core.right.RightManager.RightResult;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.runtime.parameter.Validator;
044
045/**
046 * Get information about the definition of the structure a {@link MetadataSet}.<br>
047 */
048public class GetMetadataSetDefinitionAction extends ServiceableAction
049{
050    /** The Ametys object resolver */
051    protected AmetysObjectResolver _resolver;
052    /** Content type extension point. */
053    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
054    /** The current user provider */
055    protected CurrentUserProvider _userProvider;
056    /** The rights manager */
057    protected RightManager _rightManager;
058    /** Helper for content types */
059    protected ContentTypesHelper _contentTypesHelper;
060    /** Helper for root content */
061    protected RootContentHelper _rootContentHelper;
062    
063    @Override
064    public void service(ServiceManager smanager) throws ServiceException
065    {
066        super.service(smanager);
067        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
068        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
069        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
070        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
071        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
072        _rootContentHelper = (RootContentHelper) smanager.lookup(RootContentHelper.ROLE);
073    }
074    
075    @Override
076    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
077    {
078        Request request = ObjectModelHelper.getRequest(objectModel);
079        
080        String contentTypeId = StringUtils.defaultString(request.getParameter("contentType"));
081        String contentId = StringUtils.defaultString(request.getParameter("contentId"));
082        String viewName = StringUtils.defaultIfEmpty(request.getParameter("viewName"), "default-edition");
083        String fallbackViewName = StringUtils.defaultIfEmpty(request.getParameter("fallbackViewName"), "main");
084        String metadataSetMode = StringUtils.defaultIfEmpty(request.getParameter("metadataSetMode"), "edition");
085        
086        String[] contentTypes;
087        String[] mixins = new String[0];
088        
089        MetadataSet metadataSet = null;
090        Content content = null;
091        if (StringUtils.isNotEmpty(contentTypeId))
092        {
093            contentTypes = contentTypeId.split(",");
094        }
095        else if (StringUtils.isNotEmpty(contentId))
096        {
097            content = _resolver.resolveById(contentId);
098            contentTypes = content.getTypes();
099            mixins = content.getMixinTypes();
100        }
101        else
102        {
103            String errorMsg = String.format("Unable to get metadata set definition: missing parameter 'contentId' or 'contentTypeId'");
104            getLogger().error(errorMsg);
105            throw new IllegalArgumentException(errorMsg);
106        }
107        
108        for (String id : contentTypes)
109        {
110            ContentType cType = _contentTypeExtensionPoint.getExtension(id);
111            
112            if (cType == null)
113            {
114                String errorMsg = String.format("Unable to get metadata set definition: unknown content type '" + id + "'");
115                getLogger().error(errorMsg);
116                throw new IllegalArgumentException(errorMsg);
117            }
118        }
119        
120        metadataSet = _contentTypesHelper.getMetadataSetWithFallback(viewName, fallbackViewName, contentTypes, mixins, !"view".equals(metadataSetMode));
121        
122        List<Object> metadataSetProperties = new LinkedList<>();
123        
124        if (metadataSet == null)
125        {
126            String errorMsg = String.format("Unknown metadata set '%s' of type '%s' for content types '%s'", viewName, metadataSetMode, StringUtils.join(contentTypes, ','));
127            getLogger().error(errorMsg);
128            
129            metadataSetProperties.add(metadataError2JsonObject(contentTypes, viewName, metadataSetMode));
130        }
131        else
132        {
133            // Getting the metadata set properties.
134            for (AbstractMetadataSetElement element : metadataSet.getElements())
135            {
136                Map<String, Object> childProperties = metadataSetElement2JsonObject(contentTypes, mixins, element);
137                if (childProperties != null)
138                {
139                    metadataSetProperties.add(childProperties);
140                }
141            }
142        }
143        
144        request.setAttribute(JSonReader.OBJECT_TO_READ, metadataSetProperties);
145        
146        return EMPTY_MAP;
147    }
148    
149    /**
150     * Get some error properties when the requested metadata set is not found. 
151     * @param contentTypes The content types 
152     * @param viewName The view name
153     * @param metadataSetMode The metadata set mode
154     * @return A map containing the properties.
155     */
156    protected Map<String, Object> metadataError2JsonObject(String[] contentTypes, String viewName, String metadataSetMode)
157    {
158        HashMap<String, Object> jsonObject = new LinkedHashMap<>();
159        
160        jsonObject.put("type", "error");
161        jsonObject.put("errorType", "metadataSet");
162        jsonObject.put("contentType", StringUtils.join(contentTypes, ','));
163        jsonObject.put("viewName", viewName);
164        jsonObject.put("metadataSetMode", metadataSetMode);
165        
166        jsonObject.put("children", Collections.EMPTY_LIST);
167        
168        return jsonObject;
169    }
170    
171    /* --------------------------------------------------------------------- */
172    /* -------------------- ABSTRACT METADATASET ELEMENT ------------------- */
173    /* --------------------------------------------------------------------- */
174    
175    /**
176     * Get properties of the {@link AbstractMetadataSetElement} and its child element.
177     * This method is the entry point to retrieves information of a MetadataSetElement.
178     * @param contentTypes The content types 
179     * @param mixins The mixins
180     * @param metadataSetElement The metadata set element
181     * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
182     */
183    protected Map<String, Object> metadataSetElement2JsonObject(String[] contentTypes, String[] mixins, AbstractMetadataSetElement metadataSetElement)
184    {
185        return metadataSet2JsonObject(contentTypes, mixins, null, metadataSetElement, true);
186    }
187    
188    /**
189     * Get properties of the {@link AbstractMetadataSetElement}
190     * This method is the entry point to retrieves information of a MetadataSetElement.
191     * @param contentTypes The content types 
192     * @param mixins The mixins
193     * @param metadataSetElement The metadata set element
194     * @param recurse If true, also retrieves the properties of the child elements.
195     * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
196     */
197    protected Map<String, Object> getMetadataSetElementInformation(String[] contentTypes, String[] mixins, AbstractMetadataSetElement metadataSetElement, boolean recurse)
198    {
199        return metadataSet2JsonObject(contentTypes, mixins, null, metadataSetElement, recurse);
200    }
201    
202    /**
203     * Get properties of the {@link AbstractMetadataSetElement}
204     * @param contentTypes The content types 
205     * @param mixins The mixins
206     * @param metadataDefinition The metadata definintion in recursive path. null at root.
207     * @param metadataSetElement The metadata set element
208     * @param recurse If true, also retrieves the properties of the child elements.
209     * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
210     */
211    protected Map<String, Object> metadataSet2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement, boolean recurse)
212    {
213        if (metadataSetElement instanceof MetadataDefinitionReference)
214        {
215            return metadata2JsonObject(contentTypes, mixins, metadataDefinition, (MetadataDefinitionReference) metadataSetElement, recurse);
216        }
217        else if (metadataSetElement instanceof Fieldset)
218        {
219            return fieldset2JsonObject(contentTypes, mixins, metadataDefinition, (Fieldset) metadataSetElement, recurse);
220        }
221        
222        throw new IllegalArgumentException(
223            String.format("Unexpected runtime type for the instance of the abstract class '%s' : %s", AbstractMetadataSetElement.class.getName(), metadataSetElement.getClass().getName())
224        );
225    }
226    
227    
228    /**
229     * Add informations in a map of properties for the children of a metadata set element.
230     * @param properties The map of properties to populate
231     * @param contentTypes The content types
232     * @param mixins The mixins 
233     * @param metadataDefinition The metadata definition for recursive purposes. null at root.
234     * @param metadataSetElement The metadate set.
235     */
236    protected void addChildrenInformation(Map<String, Object> properties, String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement)
237    {
238        List<Object> children = new LinkedList<>();
239        
240        for (AbstractMetadataSetElement child : metadataSetElement.getElements())
241        {
242            Map<String, Object> childProperties = metadataSet2JsonObject(contentTypes, mixins, metadataDefinition, child, true);
243            if (childProperties != null)
244            {
245                children.add(childProperties);
246            }
247        }
248        
249        properties.put("children", children);
250    }
251    
252    
253    /* --------------------------------------------------------------------- */
254    /* ------------------------------ FIELDSET ----------------------------- */
255    /* --------------------------------------------------------------------- */
256    
257    /**
258     * Get the properties of a {@link Fieldset}
259     * @param contentTypes The content types  
260     * @param mixins The mixins
261     * @param metadataDefinition The metadataset definition for recusive purposes. null at root.
262     * @param fieldset The fieldset to convert
263     * @param recurse Should it be recursive
264     * @return A map containing the {@link Fieldset} properties.
265     */
266    protected Map<String, Object> fieldset2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, Fieldset fieldset, boolean recurse)
267    {
268        HashMap<String, Object> jsonObject = new LinkedHashMap<>();
269        
270        jsonObject.put("type", "fieldset");
271        jsonObject.put("label", fieldset.getLabel());
272        jsonObject.put("role", fieldset.getRole());
273        
274        if (recurse)
275        {
276            addChildrenInformation(jsonObject, contentTypes, mixins, metadataDefinition, fieldset);
277        }
278        
279        return jsonObject;
280    }
281    
282    
283    
284    /* --------------------------------------------------------------------- */
285    /* ------------------------- METADATA REFERENCE ------------------------ */
286    /* --------------------------------------------------------------------- */
287    
288    /**
289     * Get properties of the {@link MetadataDefinition} through its {@link MetadataDefinitionReference}
290     * @param contentTypes The content types 
291     * @param mixins The mixins 
292     * @param parentMetadataDef The parent metadataref
293     * @param metadataDefRef The metadataref
294     * @param recurse True to convert recursively
295     * @return A map containing the {@link MetadataDefinitionReference} properties.
296     */
297    protected Map<String, Object> metadata2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef, boolean recurse)
298    {
299        MetadataDefinition metadataDefinition = _getMetadataDefinition(contentTypes, mixins, parentMetadataDef, metadataDefRef);
300        MetadataType type = metadataDefinition.getType();
301        
302        HashMap<String, Object> jsonObject = new LinkedHashMap<>();
303        
304        jsonObject.put("type", "metadata");
305        jsonObject.put("metadataName", metadataDefRef.getMetadataName());
306        jsonObject.put("metadataType", _getMetadataType(metadataDefinition));
307        
308        jsonObject.put("label", metadataDefinition.getLabel());
309        jsonObject.put("description", metadataDefinition.getDescription());
310        jsonObject.put("mandatory", _isMandatory(metadataDefinition));
311        
312        boolean isContentReference = MetadataType.CONTENT.equals(type);
313        boolean isSubContent = MetadataType.SUB_CONTENT.equals(type);
314        if (isSubContent || isContentReference)
315        {
316            jsonObject.put("contentType", metadataDefinition.getContentType());
317            jsonObject.put("viewName", metadataDefRef.getMetadataSetName());
318        }
319        
320        if (isContentReference)
321        {
322            jsonObject.put("canCreate", _hasRight(metadataDefinition.getContentType()));
323        }
324        
325        // Children information are not added in case of a metadata of type content.
326        if (recurse && !isSubContent && !isContentReference)
327        {
328            addChildrenInformation(jsonObject, contentTypes, mixins, metadataDefinition, metadataDefRef);
329        }
330        
331        return jsonObject;
332    }
333
334    /**
335     * Returns the type of this {@link MetadataDefinition}
336     * @param metadataDefinition The metadata defintion
337     * @return the name of type
338     */
339    protected String _getMetadataType(MetadataDefinition metadataDefinition)
340    {
341        if (metadataDefinition.getType().equals(MetadataType.COMPOSITE))
342        {
343            if (metadataDefinition instanceof RepeaterDefinition)
344            {
345                return "repeater";
346            }
347        }
348        
349        return metadataDefinition.getType().getId().toLowerCase();
350    }
351    
352    
353    /**
354     * Indicates if this {@link MetadataDefinition} is mandatory.
355     * @param metadataDefinition The metadata definition
356     * @return true if mandatory
357     */
358    protected boolean _isMandatory(MetadataDefinition metadataDefinition)
359    {
360        boolean mandatory = false;
361        
362        if (metadataDefinition.getType().equals(MetadataType.COMPOSITE) && metadataDefinition instanceof RepeaterDefinition)
363        {
364            mandatory = ((RepeaterDefinition) metadataDefinition).getMinSize() > 0;
365        }
366        else
367        {
368            Validator validator = metadataDefinition.getValidator();
369            if (validator != null)
370            {
371                mandatory = (Boolean) validator.getConfiguration().get("mandatory");
372            }
373        }
374        
375        return mandatory;
376    }
377    
378    /**
379     * Test if the current user has the right needed by the content type to create a content.
380     * @param contentTypeId The content type id
381     * @return true if the user has the right needed, false otherwise.
382     */
383    protected boolean _hasRight(String contentTypeId)
384    {
385        boolean hasRight = false;
386        
387        ContentType cType = null;
388        String right = null;
389        
390        if (contentTypeId == null)
391        {
392            return false;
393        }
394        
395        try
396        {
397            cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
398        }
399        catch (Exception e)
400        {
401            getLogger().error("Cannot check user rights because the content type has not been found : " + contentTypeId, e);
402        }
403
404        if (cType != null)
405        {
406            right = cType.getRight();
407            
408            if (right == null)
409            {
410                hasRight = true;
411            }
412            else
413            {
414                UserIdentity user = _userProvider.getUser();
415                hasRight = _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
416            }
417        }
418        else
419        {
420            getLogger().error("Cannot check user rights because the content type has not been found : " + contentTypeId);
421        }
422        
423        return hasRight;
424    }
425    
426    /**
427     * Retrieves a {@link MetadataDefinition} through its {@link MetadataDefinitionReference}
428     * @param contentTypes The content types
429     * @param mixins The mixins
430     * @param parentMetadataDef The parent metadata reference
431     * @param metadataDefRef The metadata reference
432     * @return The metadata definition found
433     */
434    private MetadataDefinition _getMetadataDefinition(String[] contentTypes, String[] mixins, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef)
435    {
436        String metadataName = metadataDefRef.getMetadataName();
437        
438        if (parentMetadataDef == null)
439        {
440            return _contentTypesHelper.getMetadataDefinition(metadataName, contentTypes, mixins);
441        }
442        else
443        {
444            return parentMetadataDef.getMetadataDefinition(metadataName);
445        }
446    }
447}