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