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.content;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Enumeration;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.parameters.Parameters;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.ProcessingException;
034import org.apache.cocoon.acting.ServiceableAction;
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Redirector;
037import org.apache.cocoon.environment.Request;
038import org.apache.cocoon.environment.SourceResolver;
039import org.apache.commons.lang3.StringUtils;
040import org.slf4j.Logger;
041
042import org.ametys.cms.content.external.ExternalizableMetadataProviderExtensionPoint;
043import org.ametys.cms.contenttype.AbstractMetadataSetElement;
044import org.ametys.cms.contenttype.ContentConstants;
045import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
046import org.ametys.cms.contenttype.ContentTypesHelper;
047import org.ametys.cms.contenttype.Fieldset;
048import org.ametys.cms.contenttype.MetadataDefinition;
049import org.ametys.cms.contenttype.MetadataDefinitionReference;
050import org.ametys.cms.contenttype.MetadataSet;
051import org.ametys.cms.contenttype.MetadataType;
052import org.ametys.cms.contenttype.RepeaterDefinition;
053import org.ametys.cms.contenttype.RichTextMetadataDefinition;
054import org.ametys.cms.contenttype.SemanticAnnotation;
055import org.ametys.cms.repository.Content;
056import org.ametys.core.cocoon.JSonReader;
057import org.ametys.core.right.RightManager;
058import org.ametys.core.right.RightManager.RightResult;
059import org.ametys.core.user.CurrentUserProvider;
060import org.ametys.core.util.AvalonLoggerAdapter;
061import org.ametys.plugins.core.ui.help.HelpManager;
062import org.ametys.runtime.i18n.I18nizableText;
063import org.ametys.runtime.parameter.ParameterHelper;
064
065/**
066 * Get metadata set definition as JSON object
067 *
068 */
069public class GetMetadataSetDefAction extends ServiceableAction implements Initializable
070{
071    /** The logger */
072    protected Logger _logger;
073    /** Content type extension point. */
074    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
075    /** Helper for content type */
076    protected ContentTypesHelper _contentTypesHelper;
077    /** The content helper */
078    protected ContentHelper _contentHelper;
079    /** Rights manager */
080    protected RightManager _rightManager;
081    /** The current user provider */
082    protected CurrentUserProvider _currentUserProvider;
083    /** The component to retrieve externalizable metadata provider */
084    protected ExternalizableMetadataProviderExtensionPoint _externalizableMetaProvider;
085    /** The help manager to get url for each property */
086    protected HelpManager _helpManager;
087    
088    @Override
089    public void service(ServiceManager serviceManager) throws ServiceException
090    {
091        super.service(serviceManager);
092        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
093        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
094        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
095        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
096        _externalizableMetaProvider = (ExternalizableMetadataProviderExtensionPoint) serviceManager.lookup(ExternalizableMetadataProviderExtensionPoint.ROLE);
097        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
098        _helpManager = (HelpManager) serviceManager.lookup(HelpManager.ROLE);
099    }
100    
101    @Override
102    public void initialize() throws Exception
103    {
104        _logger = new AvalonLoggerAdapter(getLogger());
105    }
106    
107    @SuppressWarnings("unchecked")
108    @Override
109    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
110    {
111        Request request = ObjectModelHelper.getRequest(objectModel);
112        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
113        
114        Content content = (Content) request.getAttribute(Content.class.getName());
115        boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false);
116        String metadataSetName = parameters.getParameter("metadataSetName", "main");
117        String fallbackMetadataSetName = parameters.getParameter("fallbackMetadataSetName", "");
118        List<String> metadataNames = jsParameters != null ? (List<String>) jsParameters.get("metadata") : Collections.singletonList(request.getParameter("metadata"));
119
120        List<String> languages = getLanguages(request);
121
122        Map<String, Object> jsonObject = new HashMap<>();
123        jsonObject.put("content", content2Json(content, metadataSetName, fallbackMetadataSetName, metadataNames, isEditionMetadataSet, languages));
124        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
125        return EMPTY_MAP;
126    }
127    
128    /**
129     * list all languages requested by the client in the request
130     * @param request the request
131     * @return an ordonned list of all languages requested by the client (or server default locale if none requested by the client)
132     */
133    protected List<String> getLanguages(Request request)
134    {
135        Enumeration locales = request.getLocales();
136        List<String> languages = new ArrayList<>();
137        while (locales.hasMoreElements())
138        {
139            Locale locale = (Locale) locales.nextElement();
140            String lang = locale.getLanguage();
141            if (!languages.contains(lang))
142            {
143                languages.add(lang);
144            }
145        }
146        return languages;
147    }
148
149    /**
150     * Convert content to JSON object
151     * @param content The content
152     * @param metadataSetName The metadata set name
153     * @param fallbackMetadataSetName The fallback metadata set name (if the first was not found)
154     * @param isEditionMetadataSet true if it is metadataset for edition
155     * @param languages the current languages requested by the client (an ordonned list)
156     * @return The JSON object representing the content
157     * @throws ProcessingException if an error occurred
158     */
159    protected Map<String, Object> content2Json (Content content, String metadataSetName, String fallbackMetadataSetName, boolean isEditionMetadataSet, List<String> languages) throws ProcessingException
160    {
161        return content2Json(content, metadataSetName, fallbackMetadataSetName, null, isEditionMetadataSet, languages);
162    }
163    
164    /**
165     * Convert content to JSON object
166     * @param content The content
167     * @param metadataSetName The metadata set name
168     * @param fallbackMetadataSetName The fallback metadata set name (if the first was not found)
169     * @param metadataNames If metadata name is empty, this is the list of metadatadef to get ('/' separated for composites)
170     * @param isEditionMetadataSet true if it is metadataset for edition
171     * @param languages the current languages requested by the client (an ordonned list)
172     * @return The JSON object representing the content
173     * @throws ProcessingException if an error occurred
174     */
175    protected Map<String, Object> content2Json (Content content, String metadataSetName, String fallbackMetadataSetName, List<String> metadataNames, boolean isEditionMetadataSet, List<String> languages) throws ProcessingException
176    {
177        Map<String, Object> jsonObject = new LinkedHashMap<>();
178        
179        jsonObject.put("id", content.getId());
180        jsonObject.put("name", content.getName());
181        jsonObject.put("title", _contentHelper.getTitle(content));
182        jsonObject.put("type", content.getTypes());
183        jsonObject.put("language", content.getLanguage());
184        
185        MetadataSet metadataSet = null;
186        
187        if (StringUtils.isEmpty(metadataSetName))
188        {
189            // Let us compute a metadataset
190            metadataSet = new MetadataSet();
191            metadataSet.setName("__generated__");
192            metadataSet.setLabel(new I18nizableText("Live edition metadataset"));
193            metadataSet.setDescription(new I18nizableText("Live edition metadataset"));
194            metadataSet.setSmallIcon(null);
195            metadataSet.setMediumIcon(null);
196            metadataSet.setLargeIcon(null);
197            metadataSet.setEdition(true);
198            metadataSet.setInternal(true);
199            
200            for (String metadataName : metadataNames)
201            {
202                _addMetadataDefRef(metadataSet, metadataName);
203            }
204        }
205        else
206        {
207            metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, content.getTypes(), content.getMixinTypes(), isEditionMetadataSet);
208        }
209        
210        jsonObject.put("metadataSetName", metadataSet.getName()); 
211        jsonObject.put("isEditionMetadataSet", isEditionMetadataSet); 
212        
213        Set<String> externalAndLocalMetadata = new HashSet<>();
214        if (isEditionMetadataSet)
215        {
216            externalAndLocalMetadata = _externalizableMetaProvider.getExternalAndLocalMetadata(content);
217        }
218        
219        jsonObject.put("metadataSet", metadataSetElement2JsonObject(content, null, metadataSet, externalAndLocalMetadata, languages));
220        return jsonObject;
221    }
222    
223    private void _addMetadataDefRef(AbstractMetadataSetElement metadataSetElement, String metadataName)
224    {
225        String currentLevelMetadataName = StringUtils.substringBefore(metadataName, ContentConstants.METADATA_PATH_SEPARATOR);
226        MetadataDefinitionReference metaDefRef = metadataSetElement.getMetadataDefinitionReference(currentLevelMetadataName);
227        if (metaDefRef == null)
228        {
229            metaDefRef = new MetadataDefinitionReference(currentLevelMetadataName, "main");
230            metadataSetElement.addElement(metaDefRef); 
231        }
232        
233        String subLevelMetadataName = StringUtils.substringAfter(metadataName, ContentConstants.METADATA_PATH_SEPARATOR);
234        if (StringUtils.isNotBlank(subLevelMetadataName))
235        {
236            _addMetadataDefRef(metaDefRef, subLevelMetadataName);
237        }
238    }
239    
240    /**
241     * Convert {@link AbstractMetadataSetElement} to JSON object
242     * @param content The content
243     * @param metadataDefinition The metadata definition
244     * @param metadataSetElement The metadataset element
245     * @param externalAndLocalMetadata The paths of externalizable metadata (with local and external value) 
246     * @param languages the current languages requested by the client (an ordonned list)
247     * @return The JSON object representing the metadataset element
248     * @throws ProcessingException if an error occurred
249     */
250    protected Map<String, Object> metadataSetElement2JsonObject(Content content, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement, Set<String> externalAndLocalMetadata, List<String> languages) throws ProcessingException
251    {
252        Map<String, Object> jsonObject = new LinkedHashMap<>();
253        
254        for (AbstractMetadataSetElement subMetadataSetElement : metadataSetElement.getElements())
255        {
256            if (subMetadataSetElement instanceof MetadataDefinitionReference)
257            {
258                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subMetadataSetElement;
259                String metadataName = metadataDefRef.getMetadataName();
260                MetadataDefinition metaDef = _getMetadataDefinition(content, metadataDefinition, metadataName);
261                
262                if (metaDef == null)
263                {
264                    throw new IllegalArgumentException("Unable to get the metadata definition of metadata \"" + metadataName + "\"");
265                }
266                
267                if (_contentTypesHelper.canRead(content, metaDef))
268                {
269//                    String metadataPath = StringUtils.stripStart(metaDef.getId(), "/");
270                    String metadataPath = metaDef.getId();
271                    jsonObject.put(metaDef.getName(), metadataDefinition2JsonObject(content, metadataDefRef, metaDef, metadataPath, externalAndLocalMetadata, languages));
272                }
273            }
274            else
275            {
276                if (!jsonObject.containsKey("fieldsets"))
277                {
278                    jsonObject.put("fieldsets", new ArrayList<Map<String, Object>>());
279                }
280                
281                @SuppressWarnings("unchecked")
282                List<Map<String, Object>> fieldsets = (List<Map<String, Object>>) jsonObject.get("fieldsets");
283                
284                Fieldset fieldset = (Fieldset) subMetadataSetElement;
285                
286                Map<String, Object> fieldSetObject = new LinkedHashMap<>();
287                fieldSetObject.put("role", fieldset.getRole());
288                fieldSetObject.put("label", fieldset.getLabel());
289                fieldSetObject.put("elements", metadataSetElement2JsonObject(content, metadataDefinition, fieldset, externalAndLocalMetadata, languages));
290                
291                fieldsets.add(fieldSetObject);
292            }
293        }
294        
295        return jsonObject;
296    }
297    
298    /**
299     * Convert a metadata to JSON object
300     * @param content The content 
301     * @param metadataSetElement The metadataset element
302     * @param metaDef The metadata definition
303     * @param metadataPath The path of metadata
304     * @param externalAndLocalMetadata The path of externalizable metadata (with local and external value)
305     * @param languages the current languages requested by the client (an ordonned list)
306     * @return The JSON object representing the metadata
307     * @throws ProcessingException if an error occured
308     */
309    protected Map<String, Object> metadataDefinition2JsonObject(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath, Set<String> externalAndLocalMetadata, List<String> languages) throws ProcessingException
310    {
311        Map<String, Object> jsonObject = ParameterHelper.toJSON(metaDef);
312
313        jsonObject.put("multiple", metaDef.isMultiple());
314        
315        String cTypeId = metaDef.getContentType();
316        if (cTypeId != null)
317        {
318            jsonObject.put("contentType", cTypeId);
319        }
320        String point = ContentTypeExtensionPoint.ROLE; // content type extension point
321        String contentType = metaDef.getReferenceContentType();
322        
323        String help;
324        try
325        {
326            help = _helpManager.getHelp(point, contentType + "/" + metadataPath, languages);
327            if (help != null && StringUtils.isNotBlank(help))
328            {
329                jsonObject.put("help", help);
330            }
331        }
332        catch (Exception e)
333        {
334            _logger.warn("Impossible to get help for the content type '{}' on path '{}'", contentType, metadataPath, e);
335        }
336        
337        if (externalAndLocalMetadata.contains(metadataPath))
338        {
339            // Wrap the widget
340            jsonObject.put("widget", "edition.externalizable");
341            
342            Map<String, I18nizableText> widgetParameters = new HashMap<>();
343            String widget = metaDef.getWidget();
344            if (widget != null)
345            {
346                widgetParameters.put("wrapped-widget", new I18nizableText(widget));
347            }
348            
349            Map<String, I18nizableText> wrappedWidgetParameters = metaDef.getWidgetParameters();
350            if (wrappedWidgetParameters != null && wrappedWidgetParameters.size() > 0)
351            {
352                widgetParameters.putAll(wrappedWidgetParameters);
353            }
354            jsonObject.put("widget-params", widgetParameters);
355        }
356        
357        if (metaDef.getType() == MetadataType.RICH_TEXT)
358        {
359            jsonObject.put("editableSource", _rightManager.hasRight(_currentUserProvider.getUser(), "CORE_Rights_SourceEdit", content) == RightResult.RIGHT_ALLOW); 
360        }
361        
362        
363        if (!_contentTypesHelper.canWrite(content, metaDef))
364        {
365            jsonObject.put("can-not-write", true);
366        }
367        
368        if (metaDef.getType() == MetadataType.COMPOSITE)
369        {
370            jsonObject.putAll(compositeDefinition2JsonObject(content, metadataSetElement, metaDef, externalAndLocalMetadata, languages));
371        }
372        
373        if (metaDef instanceof RichTextMetadataDefinition)
374        {
375            jsonObject.putAll(annotableDefinition2JsonObject((RichTextMetadataDefinition) metaDef));
376        }
377        
378        return jsonObject;
379    }
380    
381    private Map<String, Object> compositeDefinition2JsonObject(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, Set<String> externalAndLocalMetadata, List<String> languages) throws ProcessingException
382    {
383        Map<String, Object> jsonObject = new LinkedHashMap<>();
384        
385        if (metaDef instanceof RepeaterDefinition)
386        {
387            RepeaterDefinition repeaterDef = (RepeaterDefinition) metaDef;
388            
389            Map<String, Object> repeater = new HashMap<>();
390            
391            repeater.put("initial-size", repeaterDef.getInitialSize());
392            repeater.put("min-size", repeaterDef.getMinSize());
393            
394            int maxSize = repeaterDef.getMaxSize();
395            if (maxSize >= 0)
396            {
397                repeater.put("max-size", maxSize);
398            }
399
400            
401            repeater.put("add-label", repeaterDef.getAddLabel());
402            repeater.put("del-label", repeaterDef.getDeleteLabel());
403            repeater.put("header-label", repeaterDef.getHeaderLabel());
404            
405            repeater.put("composition", metadataSetElement2JsonObject(content, metaDef, metadataSetElement, externalAndLocalMetadata, languages));
406            
407            jsonObject.put("repeater", repeater);
408            
409        }
410        else
411        {
412            jsonObject.put("composition", metadataSetElement2JsonObject(content, metaDef, metadataSetElement, externalAndLocalMetadata, languages));
413        }
414        
415        return jsonObject;
416    }
417    
418    private Map<String, Object> annotableDefinition2JsonObject(RichTextMetadataDefinition metaDef)
419    {     
420        Map<String, Object> jsonObject = new LinkedHashMap<>();
421        
422        List<SemanticAnnotation> annotations = metaDef.getSemanticAnnotations();
423        if (annotations != null && annotations.size() > 0)
424        {
425            List<Map<String, Object>> annotationsObject = new ArrayList<>();
426            
427            for (SemanticAnnotation annotation : annotations)
428            {
429                Map<String, Object> annotationObject = new LinkedHashMap<>();
430                
431                annotationObject.put("name", annotation.getId());
432                annotationObject.put("label", annotation.getLabel());
433                annotationObject.put("description", annotation.getDescription());
434                
435                annotationsObject.add(annotationObject);
436            }
437            
438            jsonObject.put("annotations", annotationsObject);
439            
440        }
441        return jsonObject;
442    }
443    
444    /**
445     * Retrieves a sub metadata definition from a content type or
446     * a parent metadata definition. 
447     * @param content the content.
448     * @param parentMetadataDefinition the parent metadata definition.
449     * @param metadataName the metadata name.
450     * @return the metadata definition found or <code>null</code> otherwise.
451     */
452    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition parentMetadataDefinition, String metadataName)
453    {
454        MetadataDefinition metadataDefinition = null;
455        
456        if (parentMetadataDefinition == null)
457        {
458            if (content != null)
459            {
460                metadataDefinition =  _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
461            }
462        }
463        else
464        {
465            metadataDefinition = parentMetadataDefinition.getMetadataDefinition(metadataName);
466        }
467        
468        return metadataDefinition;
469    }
470
471}