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