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