001/*
002 *  Copyright 2017 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.plugins.contenttypeseditor;
017
018import java.io.IOException;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.Set;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.ProcessingException;
028import org.apache.cocoon.environment.ObjectModelHelper;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.generation.ServiceableGenerator;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.lang.StringUtils;
034import org.xml.sax.SAXException;
035
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.cms.contenttype.MetadataDefinition;
039import org.ametys.cms.contenttype.MetadataType;
040import org.ametys.cms.contenttype.RepeaterDefinition;
041import org.ametys.core.util.I18nUtils;
042import org.ametys.runtime.i18n.I18nizableText;
043import org.ametys.runtime.parameter.Validator;
044
045/**
046 * Content types export for generate a graph 
047 */
048public class ContentTypesGraphGenerator extends ServiceableGenerator
049{
050    
051    /** The request parameter name for content ids */
052    public static final String PARAMETER_CONTENTTYPE_IDS = "contenttype-ids";
053    /** The request parameter name for tree view */
054    public static final String PARAMETER_CONTENTTYPE_ISHIERARCHICALVIEW = "isHierarchicalView";
055    /** The request parameter name for export all content types */
056    public static final String PARAMETER_CONTENTTYPE_ALL = "all";
057    
058    private ContentTypeExtensionPoint _contentTypeEP;
059    
060    private I18nUtils _i18nUtils;
061    
062    @Override
063    public void service(ServiceManager serviceManager) throws ServiceException
064    {
065        super.service(serviceManager);
066        _contentTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
067        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
068    }
069    
070    @Override
071    public void generate() throws IOException, SAXException, ProcessingException
072    {
073        Request request = ObjectModelHelper.getRequest(objectModel);
074        String[] contentTypeIds = request.getParameterValues(PARAMETER_CONTENTTYPE_IDS);
075        boolean isHierarchicalView = "true".equals(request.getParameter(PARAMETER_CONTENTTYPE_ISHIERARCHICALVIEW));
076        boolean exportAllContentTypes = "true".equals(request.getParameter(PARAMETER_CONTENTTYPE_ALL));
077        
078        contentHandler.startDocument();
079        XMLUtils.startElement(contentHandler, "content-types");
080        
081        if (exportAllContentTypes)
082        {
083            Set<String> contentTypesIds = _contentTypeEP.getExtensionsIds();
084            _exportContentTypes(contentTypesIds, true, true);
085        }
086        else
087        {
088            if (contentTypeIds != null)
089            {
090                if (contentTypeIds.length < 2)
091                {
092                    String contentTypeId = contentTypeIds[0]; 
093                    if (StringUtils.isBlank(contentTypeId))
094                    {
095                        throw new IllegalArgumentException("Missing or empty '" + PARAMETER_CONTENTTYPE_IDS + "' parameter to export content type");
096                    }
097                    
098                    if (isHierarchicalView)
099                    {
100                        Set<String> contentTypes = new HashSet<>();
101                        contentTypes.add(contentTypeId);
102                        _retrieveChildrenContentTypesIds(contentTypeId, contentTypes);
103                        _exportContentTypes(contentTypes, false, false);
104                    }
105                    else
106                    {
107                        ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
108                        _addAttributesContentType(contentType);
109                        _addMetadatas(contentType);
110                        XMLUtils.endElement(contentHandler, "content-type");
111                    }
112                }
113                else
114                {
115                    Set<String> contentTypeIdSet = new HashSet<>();
116                    for (String contentTypeId : contentTypeIds)
117                    {
118                        if (StringUtils.isBlank(contentTypeId))
119                        {
120                            throw new IllegalArgumentException("Missing or empty '" + PARAMETER_CONTENTTYPE_IDS + "' parameter to export content type");
121                        }
122                        contentTypeIdSet.add(contentTypeId);
123                        if (isHierarchicalView)
124                        {
125                            _retrieveChildrenContentTypesIds(contentTypeId, contentTypeIdSet);
126                        }
127                    }
128                    _exportContentTypes(contentTypeIdSet, true, false);
129                }
130            }
131        }
132        
133        XMLUtils.endElement(contentHandler, "content-types");
134        contentHandler.endDocument();
135    }
136    
137    /**
138     * Retrieve content types ids for all children of content type with contentTypeId id
139     * @param contentTypeId The content type id of content type that we want to retrieves all children content types ids
140     * @param childrenContentTypesIds All children content types ids retrieved
141     */
142    private void _retrieveChildrenContentTypesIds(String contentTypeId, Set<String> childrenContentTypesIds)
143    {
144        Set<String> directSubTypes = _contentTypeEP.getDirectSubTypes(contentTypeId);
145        for (String id : directSubTypes)
146        {
147            childrenContentTypesIds.add(id);
148            _retrieveChildrenContentTypesIds(id, childrenContentTypesIds); 
149        }
150    }
151    
152    /**
153     * Export content types
154     * @param contentTypesIds The content types ids to export
155     * @param addAssociations Allow to create a link association when a content type contains a Content metadata type
156     * @param addParents Allow to add all parents of content type even if the parent content type wasn't selected
157     * @throws SAXException If an error occured while saxing
158     */
159    private void _exportContentTypes(Set<String> contentTypesIds, boolean addAssociations, boolean addParents) throws SAXException
160    {
161        for (String id : contentTypesIds)
162        {
163            ContentType contentType = _contentTypeEP.getExtension(id);
164            _addAttributesContentType(contentType);
165            _addMetadatas(contentType);
166            if (addAssociations)
167            {
168                _addAssociations(contentType, contentTypesIds);
169            }
170            String[] supertypeIds = contentType.getSupertypeIds();
171            Set<ContentType> parentsContentTypes = new HashSet<>();
172            for (String supertypeId : supertypeIds)
173            {
174                if (addParents || contentTypesIds.contains(supertypeId))
175                {
176                    parentsContentTypes.add(_contentTypeEP.getExtension(supertypeId));
177                }
178            }
179            _addParents(parentsContentTypes);
180            XMLUtils.endElement(contentHandler, "content-type");
181        }
182    }
183    
184    private void _addAttributesContentType(ContentType contentType) throws SAXException
185    {
186        AttributesImpl attrs = new AttributesImpl();
187        attrs.addCDATAAttribute("name", _getContentTypeName(contentType.getLabel()));
188        attrs.addCDATAAttribute("id", _getIdForGraph(contentType.getId()));
189        attrs.addCDATAAttribute("abstract", Boolean.toString(contentType.isAbstract()));
190        attrs.addCDATAAttribute("private", Boolean.toString(contentType.isPrivate()));
191        attrs.addCDATAAttribute("mixin", Boolean.toString(contentType.isMixin()));
192        attrs.addCDATAAttribute("referencetable", Boolean.toString(contentType.isReferenceTable()));
193        attrs.addCDATAAttribute("multilingual", Boolean.toString(contentType.isMultilingual()));
194        XMLUtils.startElement(contentHandler, "content-type", attrs);
195    }
196    
197    private void _addMetadatas(ContentType contentType) throws SAXException
198    {
199        XMLUtils.startElement(contentHandler, "metadatas");
200        Set<String> metadataNames = contentType.getMetadataNames();
201        for (String string : metadataNames)
202        {
203            MetadataDefinition metadataDefinition = contentType.getMetadataDefinition(string); 
204            _addMetadata(metadataDefinition, contentType);
205        }
206        XMLUtils.endElement(contentHandler, "metadatas");
207    }
208    
209    private void _addMetadata(MetadataDefinition metadataDefinition, ContentType contentType) throws SAXException
210    {
211        String referenceContentType = metadataDefinition.getReferenceContentType();
212        if (referenceContentType.equals(contentType.getId())) 
213        {
214            MetadataType metadataType = metadataDefinition.getType();
215            String type = metadataType.name().toLowerCase();
216            String name = _i18nUtils.translate(metadataDefinition.getLabel());
217            name = name == null ? metadataDefinition.getLabel().toString() : name;
218            
219            AttributesImpl attrs = new AttributesImpl();
220            attrs.addCDATAAttribute("multiple",  Boolean.toString(metadataDefinition.isMultiple()));
221            attrs.addCDATAAttribute("mandatory", Boolean.toString(_isMandatoryMetadata(metadataDefinition)));
222            if (metadataType == MetadataType.COMPOSITE)
223            {
224                if (metadataDefinition instanceof RepeaterDefinition)
225                {
226                    type = "repeater";
227                }
228                attrs.addCDATAAttribute("type", type);
229                XMLUtils.startElement(contentHandler, "metadata", attrs);
230                XMLUtils.createElement(contentHandler, "name", name);
231                Set<String> metadataNames = metadataDefinition.getMetadataNames();
232                for (String metadata : metadataNames)
233                {
234                    MetadataDefinition childMetadataDefinition = metadataDefinition.getMetadataDefinition(metadata);
235                    _addMetadata(childMetadataDefinition, contentType);
236                }
237                XMLUtils.endElement(contentHandler, "metadata");
238            }
239            else
240            {
241                if (metadataType == MetadataType.CONTENT)
242                {
243                    String linkedContentTypeId = metadataDefinition.getContentType();
244                    if (linkedContentTypeId != null)
245                    {
246                        ContentType linkedContent = _contentTypeEP.getExtension(metadataDefinition.getContentType());
247                        attrs.addCDATAAttribute("content-type", _getContentTypeName(linkedContent.getLabel()));
248                    }
249                    else
250                    {
251                        attrs.addCDATAAttribute("content-type", type);
252                    }
253                    attrs.addCDATAAttribute("invert", _getInvert(metadataDefinition));
254                }
255                attrs.addCDATAAttribute("type", type);
256                XMLUtils.createElement(contentHandler, "metadata", attrs, name); 
257            }
258        }
259    }
260    
261    private void _addParents(Set<ContentType> parentsContentTypes) throws SAXException
262    {
263        XMLUtils.startElement(contentHandler, "parents");
264        for (ContentType contentType : parentsContentTypes)
265        {
266            AttributesImpl attrs = new AttributesImpl();
267            attrs.addCDATAAttribute("id", _getIdForGraph(contentType.getId()));
268            XMLUtils.createElement(contentHandler, "parent", attrs, _getContentTypeName(contentType.getLabel()));
269        }
270        XMLUtils.endElement(contentHandler, "parents");
271    }
272    
273    private void _addAssociations(ContentType contentType, Set<String> contentTypesIds) throws SAXException
274    {
275        Map<ContentType, String> linkedContents = new HashMap<>();
276      
277        XMLUtils.startElement(contentHandler, "associations");
278
279        Set<String> metadataNames = contentType.getMetadataNames();
280        for (String string : metadataNames)
281        {
282            MetadataDefinition metadataDefinition = contentType.getMetadataDefinition(string);
283            String referenceContentType = metadataDefinition.getReferenceContentType();
284            if (referenceContentType.equals(contentType.getId())) 
285            {
286                _retrieveLinkedContents(metadataDefinition, contentTypesIds, linkedContents);
287            }
288        }
289        for (Entry<ContentType, String> linkedContent : linkedContents.entrySet())
290        {
291            AttributesImpl attrs = new AttributesImpl();
292            attrs.addCDATAAttribute("cardinality", linkedContent.getValue());
293            XMLUtils.createElement(contentHandler, "association", attrs, _getIdForGraph(linkedContent.getKey().getId()));
294        }
295        
296        XMLUtils.endElement(contentHandler, "associations");
297    }
298    
299    private void _retrieveLinkedContents(MetadataDefinition metadataDefinition, Set<String> contentTypesIds, Map<ContentType, String> linkedContents)
300    {
301        if (metadataDefinition.getType() == MetadataType.COMPOSITE)
302        {
303            Set<String> compositeMetadataNames = metadataDefinition.getMetadataNames();
304            for (String compositeMetadata : compositeMetadataNames)
305            {
306                MetadataDefinition childMetadataDefinition = metadataDefinition.getMetadataDefinition(compositeMetadata);
307                _retrieveLinkedContents(childMetadataDefinition, contentTypesIds, linkedContents);
308            }
309        }
310        else if (metadataDefinition.getType() == MetadataType.CONTENT)
311        {
312            String contentTypeId = metadataDefinition.getContentType();
313            if (contentTypeId != null)
314            {
315                if (contentTypesIds.contains(contentTypeId))
316                {
317                    ContentType linkedContent = _contentTypeEP.getExtension(contentTypeId);
318                    if (!linkedContents.containsKey(linkedContent))
319                    {
320                        linkedContents.put(linkedContent, getCardinality(metadataDefinition));
321                    }
322                    else 
323                    {
324                        linkedContents.put(linkedContent, "");
325                    }
326                }
327            }
328        }
329    }
330    
331    private String getCardinality(MetadataDefinition metadataDefinition)
332    {
333        String cardinality = "";
334        cardinality += _isMandatoryMetadata(metadataDefinition) ? "1" : "0";
335        if (metadataDefinition.isMultiple())
336        {
337            cardinality += "..*";
338        }
339        else
340        {
341            if (!_isMandatoryMetadata(metadataDefinition)) 
342            {
343                cardinality += "..1";
344            }
345        }
346        return cardinality;
347    }
348    
349    private String _getInvert(MetadataDefinition metadataDefinition)
350    {
351        String invert = metadataDefinition.getInvertRelationPath();
352        if (invert == null)
353        {
354            invert = "";
355        }
356        else
357        {
358            ContentType linkedContent = _contentTypeEP.getExtension(metadataDefinition.getContentType());
359            MetadataDefinition linkedMetadataDefinition = linkedContent.getMetadataDefinitionByPath(invert);
360            invert = _getContentTypeName(linkedMetadataDefinition.getLabel());
361        }
362        return invert;
363    }
364    
365    private String _getContentTypeName(I18nizableText labelContentType)
366    {
367        String translatedLabel = _i18nUtils.translate(labelContentType);
368        String name = translatedLabel == null ? labelContentType.toString() : translatedLabel;
369        return name;
370    }
371    
372    private String _getIdForGraph(String text)
373    {
374        String result = ""; 
375        result = text.replaceAll("\\s", "_");
376        result = text.replaceAll("[^\\p{L}&&[^0-9]]", "_");
377        return result;
378    }
379    
380    private boolean _isMandatoryMetadata(MetadataDefinition metadataDefinition)
381    {
382        boolean isMandatory = false;
383        Validator validator = metadataDefinition.getValidator();
384        if (validator != null)
385        {
386            Map<String, Object> configuration = validator.getConfiguration();
387            if (configuration != null)
388            {
389                Object object = configuration.get("mandatory");
390                if (object instanceof Boolean)
391                {
392                    isMandatory = (boolean) object;
393                }
394            }
395        }
396        return isMandatory;
397    }
398
399}