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.ParameterHelper;
058
059/**
060 * Get metadata set definition as JSON object
061 *
062 */
063public class GetMetadataSetDefAction extends ServiceableAction
064{
065    /** Content type extension point. */
066    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
067    /** Helper for content type */
068    protected ContentTypesHelper _contentTypesHelper;
069    /** The content helper */
070    protected ContentHelper _contentHelper;
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        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
088    }
089    
090    @SuppressWarnings("unchecked")
091    @Override
092    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
093    {
094        Request request = ObjectModelHelper.getRequest(objectModel);
095        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
096        
097        Content content = (Content) request.getAttribute(Content.class.getName());
098        boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false);
099        String metadataSetName = parameters.getParameter("metadataSetName", "main");
100        List<String> metadataNames = jsParameters != null ? (List<String>) jsParameters.get("metadata") : Collections.singletonList(request.getParameter("metadata"));
101                
102        Map<String, Object> jsonObject = new HashMap<>();
103        jsonObject.put("content", content2Json(content, metadataSetName, metadataNames, isEditionMetadataSet));
104        
105        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
106        return EMPTY_MAP;
107    }
108
109    /**
110     * Convert content to JSON object
111     * @param content The content
112     * @param metadataSetName The metadata set name
113     * @param isEditionMetadataSet true if it is metadataset for edition
114     * @return The JSON object representing the content
115     * @throws ProcessingException if an error occurred
116     */
117    protected Map<String, Object> content2Json (Content content, String metadataSetName, boolean isEditionMetadataSet) throws ProcessingException
118    {
119        return content2Json(content, metadataSetName, null, isEditionMetadataSet);
120    }
121    
122    /**
123     * Convert content to JSON object
124     * @param content The content
125     * @param metadataSetName The metadata set name
126     * @param metadataNames If metadata name is empty, this is the list of metadatadef to get ('/' separated for composites)
127     * @param isEditionMetadataSet true if it is metadataset for edition
128     * @return The JSON object representing the content
129     * @throws ProcessingException if an error occurred
130     */
131    protected Map<String, Object> content2Json (Content content, String metadataSetName, List<String> metadataNames, boolean isEditionMetadataSet) throws ProcessingException
132    {
133        Map<String, Object> jsonObject = new LinkedHashMap<>();
134        
135        jsonObject.put("id", content.getId());
136        jsonObject.put("name", content.getName());
137        jsonObject.put("title", _contentHelper.getTitle(content));
138        jsonObject.put("type", content.getTypes());
139        jsonObject.put("language", content.getLanguage());
140        
141        MetadataSet metadataSet = null;
142        
143        if (StringUtils.isEmpty(metadataSetName))
144        {
145            // Let us compute a metadataset
146            metadataSet = new MetadataSet();
147            metadataSet.setName("__generated__");
148            metadataSet.setLabel(new I18nizableText("Live edition metadataset"));
149            metadataSet.setDescription(new I18nizableText("Live edition metadataset"));
150            metadataSet.setSmallIcon(null);
151            metadataSet.setMediumIcon(null);
152            metadataSet.setLargeIcon(null);
153            metadataSet.setEdition(true);
154            metadataSet.setInternal(true);
155            
156            for (String metadataName : metadataNames)
157            {
158                _addMetadataDefRef(metadataSet, metadataName);
159            }
160        }
161        else
162        {
163            if (isEditionMetadataSet)
164            {
165                metadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
166            }
167            else
168            {
169                metadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
170            }
171        }
172        
173        if (metadataSet == null)
174        {
175            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type(s) '%s'",
176                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ','))); 
177        }
178        
179        jsonObject.put("metadataSetName", metadataSetName); 
180        jsonObject.put("isEditionMetadataSet", isEditionMetadataSet); 
181        
182        Set<String> externalAndLocalMetadata = new HashSet<>();
183        if (isEditionMetadataSet)
184        {
185            externalAndLocalMetadata = _externalizableMetaProvider.getExternalAndLocalMetadata(content);
186        }
187        
188        jsonObject.put("metadataSet", metadataSetElement2JsonObject(content, null, metadataSet, externalAndLocalMetadata));
189        return jsonObject;
190    }
191    
192    private void _addMetadataDefRef(AbstractMetadataSetElement metadataSetElement, String metadataName)
193    {
194        String currentLevelMetadataName = StringUtils.substringBefore(metadataName, ContentConstants.METADATA_PATH_SEPARATOR);
195        MetadataDefinitionReference metaDefRef = metadataSetElement.getMetadataDefinitionReference(currentLevelMetadataName);
196        if (metaDefRef == null)
197        {
198            metaDefRef = new MetadataDefinitionReference(currentLevelMetadataName, "main");
199            metadataSetElement.addElement(metaDefRef); 
200        }
201        
202        String subLevelMetadataName = StringUtils.substringAfter(metadataName, ContentConstants.METADATA_PATH_SEPARATOR);
203        if (StringUtils.isNotBlank(subLevelMetadataName))
204        {
205            _addMetadataDefRef(metaDefRef, subLevelMetadataName);
206        }
207    }
208    
209    /**
210     * Convert {@link AbstractMetadataSetElement} to JSON object
211     * @param content The content
212     * @param metadataDefinition The metadata definition
213     * @param metadataSetElement The metadataset element
214     * @param externalAndLocalMetadata The paths of externalizable metadata (with local and external value) 
215     * @return The JSON object representing the metadataset element
216     * @throws ProcessingException if an error occurred
217     */
218    protected Map<String, Object> metadataSetElement2JsonObject(Content content, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement, Set<String> externalAndLocalMetadata) throws ProcessingException
219    {
220        Map<String, Object> jsonObject = new LinkedHashMap<>();
221        
222        for (AbstractMetadataSetElement subMetadataSetElement : metadataSetElement.getElements())
223        {
224            if (subMetadataSetElement instanceof MetadataDefinitionReference)
225            {
226                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subMetadataSetElement;
227                String metadataName = metadataDefRef.getMetadataName();
228                MetadataDefinition metaDef = _getMetadataDefinition(content, metadataDefinition, metadataName);
229                
230                if (metaDef == null)
231                {
232                    throw new IllegalArgumentException("Unable to get the metadata definition of metadata \"" + metadataName + "\"");
233                }
234                
235                if (_contentTypesHelper.canRead(content, metaDef))
236                {
237//                    String metadataPath = StringUtils.stripStart(metaDef.getId(), "/");
238                    String metadataPath = metaDef.getId();
239                    jsonObject.put(metaDef.getName(), metadataDefinition2JsonObject(content, metadataDefRef, metaDef, metadataPath, externalAndLocalMetadata));
240                }
241            }
242            else
243            {
244                if (!jsonObject.containsKey("fieldsets"))
245                {
246                    jsonObject.put("fieldsets", new ArrayList<Map<String, Object>>());
247                }
248                
249                @SuppressWarnings("unchecked")
250                List<Map<String, Object>> fieldsets = (List<Map<String, Object>>) jsonObject.get("fieldsets");
251                
252                Fieldset fieldset = (Fieldset) subMetadataSetElement;
253                
254                Map<String, Object> fieldSetObject = new LinkedHashMap<>();
255                fieldSetObject.put("role", fieldset.getRole());
256                fieldSetObject.put("label", fieldset.getLabel());
257                fieldSetObject.put("elements", metadataSetElement2JsonObject(content, metadataDefinition, fieldset, externalAndLocalMetadata));
258                
259                fieldsets.add(fieldSetObject);
260            }
261        }
262        
263        return jsonObject;
264    }
265    
266    /**
267     * Convert a metadata to JSON object
268     * @param content The content 
269     * @param metadataSetElement The metadataset element
270     * @param metaDef The metadata definition
271     * @param metadataPath The path of metadata
272     * @param externalAndLocalMetadata The path of externalizable metadata (with local and external value)
273     * @return The JSON object representing the metadata
274     * @throws ProcessingException if an error occured
275     */
276    protected Map<String, Object> metadataDefinition2JsonObject(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath, Set<String> externalAndLocalMetadata) throws ProcessingException
277    {
278        Map<String, Object> jsonObject = ParameterHelper.toJSON(metaDef);
279        
280        jsonObject.put("multiple", metaDef.isMultiple());
281        
282        String cTypeId = metaDef.getContentType();
283        if (cTypeId != null)
284        {
285            jsonObject.put("contentType", cTypeId);
286        }
287        
288        if (externalAndLocalMetadata.contains(metadataPath))
289        {
290            // Wrap the widget
291            jsonObject.put("widget", "edition.externalizable");
292            
293            Map<String, I18nizableText> widgetParameters = new HashMap<>();
294            String widget = metaDef.getWidget();
295            if (widget != null)
296            {
297                widgetParameters.put("wrapped-widget", new I18nizableText(widget));
298            }
299            
300            Map<String, I18nizableText> wrappedWidgetParameters = metaDef.getWidgetParameters();
301            if (wrappedWidgetParameters != null && wrappedWidgetParameters.size() > 0)
302            {
303                widgetParameters.putAll(wrappedWidgetParameters);
304            }
305            jsonObject.put("widget-params", widgetParameters);
306        }
307        
308        if (metaDef.getType() == MetadataType.RICH_TEXT)
309        {
310            jsonObject.put("editableSource", _rightManager.hasRight(_currentUserProvider.getUser(), "CORE_Rights_SourceEdit", content) == RightResult.RIGHT_ALLOW); 
311        }
312        
313        
314        if (!_contentTypesHelper.canWrite(content, metaDef))
315        {
316            jsonObject.put("can-not-write", true);
317        }
318        
319        if (metaDef.getType() == MetadataType.COMPOSITE)
320        {
321            jsonObject.putAll(compositeDefinition2JsonObject(content, metadataSetElement, metaDef, externalAndLocalMetadata));
322        }
323        
324        if (metaDef instanceof RichTextMetadataDefinition)
325        {
326            jsonObject.putAll(annotableDefinition2JsonObject((RichTextMetadataDefinition) metaDef));
327        }
328        
329        return jsonObject;
330    }
331    
332    private Map<String, Object> compositeDefinition2JsonObject(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, Set<String> externalAndLocalMetadata) throws ProcessingException
333    {
334        Map<String, Object> jsonObject = new LinkedHashMap<>();
335        
336        if (metaDef instanceof RepeaterDefinition)
337        {
338            RepeaterDefinition repeaterDef = (RepeaterDefinition) metaDef;
339            
340            Map<String, Object> repeater = new HashMap<>();
341            
342            repeater.put("initial-size", repeaterDef.getInitialSize());
343            repeater.put("min-size", repeaterDef.getMinSize());
344            
345            int maxSize = repeaterDef.getMaxSize();
346            if (maxSize >= 0)
347            {
348                repeater.put("max-size", maxSize);
349            }
350
351            
352            repeater.put("add-label", repeaterDef.getAddLabel());
353            repeater.put("del-label", repeaterDef.getDeleteLabel());
354            repeater.put("header-label", repeaterDef.getHeaderLabel());
355            
356            repeater.put("composition", metadataSetElement2JsonObject(content, metaDef, metadataSetElement, externalAndLocalMetadata));
357            
358            jsonObject.put("repeater", repeater);
359            
360        }
361        else
362        {
363            jsonObject.put("composition", metadataSetElement2JsonObject(content, metaDef, metadataSetElement, externalAndLocalMetadata));
364        }
365        
366        return jsonObject;
367    }
368    
369    private Map<String, Object> annotableDefinition2JsonObject(RichTextMetadataDefinition metaDef)
370    {     
371        Map<String, Object> jsonObject = new LinkedHashMap<>();
372        
373        List<SemanticAnnotation> annotations = metaDef.getSemanticAnnotations();
374        if (annotations != null && annotations.size() > 0)
375        {
376            List<Map<String, Object>> annotationsObject = new ArrayList<>();
377            
378            for (SemanticAnnotation annotation : annotations)
379            {
380                Map<String, Object> annotationObject = new LinkedHashMap<>();
381                
382                annotationObject.put("name", annotation.getId());
383                annotationObject.put("label", annotation.getLabel());
384                annotationObject.put("description", annotation.getDescription());
385                
386                annotationsObject.add(annotationObject);
387            }
388            
389            jsonObject.put("annotations", annotationsObject);
390            
391        }
392        return jsonObject;
393    }
394    
395    /**
396     * Retrieves a sub metadata definition from a content type or
397     * a parent metadata definition. 
398     * @param content the content.
399     * @param parentMetadataDefinition the parent metadata definition.
400     * @param metadataName the metadata name.
401     * @return the metadata definition found or <code>null</code> otherwise.
402     */
403    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition parentMetadataDefinition, String metadataName)
404    {
405        MetadataDefinition metadataDefinition = null;
406        
407        if (parentMetadataDefinition == null)
408        {
409            if (content != null)
410            {
411                metadataDefinition =  _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
412            }
413        }
414        else
415        {
416            metadataDefinition = parentMetadataDefinition.getMetadataDefinition(metadataName);
417        }
418        
419        return metadataDefinition;
420    }
421
422}