001/*
002 *  Copyright 2010 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.clientsideelement;
017
018import java.util.ArrayList;
019import java.util.Comparator;
020import java.util.HashSet;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.TreeMap;
027import java.util.TreeSet;
028
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.configuration.DefaultConfiguration;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034
035import org.ametys.cms.content.RootContentHelper;
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.cms.languages.LanguagesManager;
039import org.ametys.core.right.RightManager.RightResult;
040import org.ametys.core.ui.ClientSideElement;
041import org.ametys.core.ui.SimpleMenu;
042import org.ametys.core.ui.StaticClientSideElement;
043import org.ametys.core.user.UserIdentity;
044import org.ametys.core.util.I18nUtils;
045import org.ametys.core.util.I18nizableTextKeyComparator;
046import org.ametys.runtime.i18n.I18nizableText;
047
048/**
049 * This element creates a menu with one gallery item per content type classified by category (default mode).
050 *  
051 * This element supports an alternative display, to display content types into sub menu items (root menu items will be the categories),
052 * if 'show-in-menu' parameters is set to 'true'. This mode should be privileged if the number of content types is important (> 30)
053 * 
054 * The user rights are checked. 
055 */
056public class ContentTypesGallery extends SimpleMenu
057{
058    /** The list of content types */
059    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
060    /** The language manager */
061    protected LanguagesManager _languagesManager;
062    
063    /** The i18n utils */
064    protected I18nUtils _i18nUtils;
065    
066    /** Helper for root content */
067    protected RootContentHelper _rootContentHelper;
068    
069    private boolean _contentTypesInitialized;
070    
071    private Set<String> _addedComponents;
072    
073    @Override
074    public void service(ServiceManager smanager) throws ServiceException
075    {
076        super.service(smanager);
077        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
078        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
079        _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE);
080        _rootContentHelper = (RootContentHelper) smanager.lookup(RootContentHelper.ROLE);
081    }
082    
083    @Override
084    protected Script _configureScript(Configuration configuration) throws ConfigurationException
085    {
086        Script script = super._configureScript(configuration);
087        
088        // Add the CSS brought by content types
089        for (String id: _contentTypeExtensionPoint.getExtensionsIds())
090        {
091            ContentType contentType = _contentTypeExtensionPoint.getExtension(id);
092            script.getCSSFiles().addAll(contentType.getCSSFiles());
093        }
094        
095        return script;
096    }
097    
098    @Override
099    protected void _getGalleryItems(Map<String, Object> parameters, Map<String, Object> contextualParameters)
100    {
101        if (!_showInMenu())
102        {
103            // In this mode (default mode), the available content types will be displayed into gallery items, classified by category
104
105            try
106            {
107                _lazyInitializeContentTypeGallery();
108            }
109            catch (Exception e)
110            {
111                throw new IllegalStateException("Unable to lookup client side element local components", e);
112            }
113            
114            if (_galleryItems.size() > 0)
115            {
116                parameters.put("gallery-item", new LinkedHashMap<String, Object>());
117                
118                for (GalleryItem galleryItem : _galleryItems)
119                {
120                    @SuppressWarnings("unchecked")
121                    Map<String, Object> galleryItems = (Map<String, Object>) parameters.get("gallery-item");
122                    galleryItems.put("gallery-groups", new ArrayList<>());
123                    
124                    for (GalleryGroup galleryGp : galleryItem.getGroups())
125                    {
126                        @SuppressWarnings("unchecked")
127                        List<Object> galleryGroups = (List<Object>) galleryItems.get("gallery-groups");
128                        
129                        Map<String, Object> groupParams = new LinkedHashMap<>();
130                        
131                        I18nizableText label = galleryGp.getLabel();
132                        groupParams.put("label", label);
133                        
134                        // Group items
135                        List<String> gpItems = new ArrayList<>();
136                        for (ClientSideElement element : galleryGp.getItems())
137                        {
138                            String cTypeId = element.getId().substring(this.getId().length() + 1);
139                            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
140                            
141                            if (hasRight(cType))
142                            {
143                                gpItems.add(element.getId());
144                            }
145                        }
146                        
147                        if (gpItems.size() > 0)
148                        {
149                            groupParams.put("items", gpItems);
150                            galleryGroups.add(groupParams);
151                        }
152                    }
153                }
154            }
155        }
156    }
157    
158    private boolean _showInMenu()
159    {
160        return _script.getParameters().containsKey("show-in-menu") && _script.getParameters().get("show-in-menu").equals("true");
161    }
162    
163    private void _lazyInitializeContentTypeGallery() throws ConfigurationException
164    {
165        if (!_contentTypesInitialized)
166        {
167            _addedComponents = new HashSet<>();
168            
169            Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup();
170            
171            if (cTypesByGroup.size() > 0)
172            {
173                GalleryItem galleryItem = new GalleryItem();
174                
175                for (I18nizableText groupLabel : cTypesByGroup.keySet())
176                {
177                    GalleryGroup galleryGroup = new GalleryGroup(groupLabel);
178                    galleryItem.addGroup(galleryGroup);
179                    
180                    Set<ContentType> cTypes = cTypesByGroup.get(groupLabel);
181                    for (ContentType cType : cTypes)
182                    {
183                        String id = this.getId() + "-" + cType.getId();
184                        
185                        DefaultConfiguration conf = new DefaultConfiguration("extension");
186                        conf.setAttribute("id", id);
187                        _addContentTypeConfiguration (conf, cType);
188                        
189                        if (!_addedComponents.contains(id))
190                        {
191                            _galleryItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
192                            galleryGroup.addItem(new UnresolvedItem(id, true));
193                            _addedComponents.add(id);
194                        }
195                    }
196                }
197                
198                _galleryItems.add(galleryItem);
199            }
200            
201            if (_galleryItems.size() > 0)
202            {
203                try
204                {
205                    _galleryItemManager.initialize();
206                }
207                catch (Exception e)
208                {
209                    throw new ConfigurationException("Unable to lookup parameter local components", e);
210                }
211            }
212        }
213        
214        _contentTypesInitialized = true;
215    }
216    
217    private void _lazyInitializeContentTypeMenu()
218    {
219        if (!_contentTypesInitialized)
220        {
221            _addedComponents = new HashSet<>();
222            
223            Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup();
224            
225            if (cTypesByGroup.size() > 0)
226            {
227                for (I18nizableText groupLabel : cTypesByGroup.keySet())
228                {
229                    String id = this.getId() + "-" + org.ametys.core.util.StringUtils.generateKey();
230                    DefaultConfiguration conf = new DefaultConfiguration("extension");
231                    conf.setAttribute("id", id);
232                    
233                    Set<ContentType> cTypes = cTypesByGroup.get(groupLabel);
234                    _addGroupContentTypeConfiguration (conf, groupLabel, cTypes);
235                    
236                    if (!_addedComponents.contains(id))
237                    {
238                        _menuItemManager.addComponent(_pluginName, null, id, SimpleMenu.class, conf);
239                        _unresolvedMenuItems.add(new UnresolvedItem(id, true));
240                        _addedComponents.add(id);
241                    }
242                }
243            }
244        }
245        
246        _contentTypesInitialized = true;
247    }
248    
249    /**
250     * Get the configuration of the content type item
251     * @param rootConf The root configuration
252     * @param groupLabel The group's label
253     * @param cTypes The content types of the group
254     */
255    protected void _addGroupContentTypeConfiguration (DefaultConfiguration rootConf, I18nizableText groupLabel, Set<ContentType> cTypes)
256    {
257        DefaultConfiguration classConf = new DefaultConfiguration("class");
258        classConf.setAttribute("name", "Ametys.plugins.cms.content.controller.ContentTypeMenuItemController");
259        
260        // Label and description
261        DefaultConfiguration labelConf = new DefaultConfiguration("label");
262        DefaultConfiguration descConf = new DefaultConfiguration("description");
263        if (groupLabel.isI18n())
264        {
265            labelConf.setAttribute("i18n", "true");
266            labelConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey());
267            descConf.setAttribute("i18n", "true");
268            descConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey());
269        }
270        else
271        {
272            labelConf.setValue(groupLabel.getLabel());
273            descConf.setValue(groupLabel.getLabel());
274        }
275        classConf.addChild(labelConf);
276        classConf.addChild(descConf);
277        
278        // Icons or glyph (use the first content type of the list)
279        ContentType firstCtype = cTypes.iterator().next();
280        if (firstCtype.getIconGlyph() != null)
281        {
282            DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph");
283            iconGlyphConf.setValue(firstCtype.getIconGlyph());
284            classConf.addChild(iconGlyphConf);
285        }
286        if (firstCtype.getIconDecorator() != null)
287        {
288            DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator");
289            iconDecoratorConf.setValue(firstCtype.getIconDecorator());
290            classConf.addChild(iconDecoratorConf);
291        }
292        if (firstCtype.getSmallIcon() != null)
293        {
294            DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
295            iconSmallConf.setValue(firstCtype.getSmallIcon());
296            classConf.addChild(iconSmallConf);
297            DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
298            iconMediumConf.setValue(firstCtype.getMediumIcon());
299            classConf.addChild(iconMediumConf);
300            DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
301            iconLargeConf.setValue(firstCtype.getLargeIcon());
302            classConf.addChild(iconLargeConf);
303        }
304        
305        // Common configuration
306        @SuppressWarnings("unchecked")
307        Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("group-items-config");
308        for (String tagName : commonConfig.keySet())
309        {
310            DefaultConfiguration c = new DefaultConfiguration(tagName);
311            c.setValue(String.valueOf(commonConfig.get(tagName)));
312            classConf.addChild(c);
313        }
314        
315        rootConf.addChild(classConf);
316        
317        // Menu items
318        DefaultConfiguration menuItemsConf = new DefaultConfiguration("menu-items");
319        
320        for (ContentType contentType : cTypes)
321        {
322            DefaultConfiguration menuItemConf = new DefaultConfiguration("item");
323            menuItemConf.setAttribute("id", this.getId() + "-" + contentType.getId());
324            _addContentTypeConfiguration (menuItemConf, contentType);
325            
326            menuItemsConf.addChild(menuItemConf);
327        }
328        
329        rootConf.addChild(menuItemsConf);
330    }
331    
332    /**
333     * Get the configuration of the content type item
334     * @param rootConf The root configuration
335     * @param cType The content type
336     */
337    protected void _addContentTypeConfiguration (DefaultConfiguration rootConf, ContentType cType)
338    {
339        DefaultConfiguration classConf = new DefaultConfiguration("class");
340        classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController");
341        
342        // Parent controller id
343        DefaultConfiguration parentIdConf = new DefaultConfiguration("controllerParentId");
344        parentIdConf.setValue(this.getId());
345        classConf.addChild(parentIdConf);
346        
347        // Label
348        DefaultConfiguration labelConf = new DefaultConfiguration("label");
349        I18nizableText label = cType.getLabel();
350        if (label.isI18n())
351        {
352            labelConf.setAttribute("i18n", "true");
353            labelConf.setValue(label.getCatalogue() + ":" + label.getKey());
354        }
355        else
356        {
357            labelConf.setValue(label.getLabel());
358        }
359        classConf.addChild(labelConf);
360        
361        // Description
362        DefaultConfiguration descConf = new DefaultConfiguration("description");
363        I18nizableText description = cType.getDescription();
364        if (description.isI18n())
365        {
366            descConf.setAttribute("i18n", "true");
367            descConf.setValue(description.getCatalogue() + ":" + description.getKey());
368        }
369        else
370        {
371            descConf.setValue(description.getLabel());
372        }
373        classConf.addChild(descConf);
374        
375        // Default content title
376        DefaultConfiguration defaultTitleConf = new DefaultConfiguration("defaultContentTitle");
377        I18nizableText defaultTitle = cType.getDefaultTitle();
378        if (defaultTitle.isI18n())
379        {
380            defaultTitleConf.setAttribute("i18n", "true");
381            defaultTitleConf.setValue(defaultTitle.getCatalogue() + ":" + defaultTitle.getKey());
382        }
383        else
384        {
385            defaultTitleConf.setValue(defaultTitle.getLabel());
386        }
387        classConf.addChild(defaultTitleConf);
388        
389        // Content type
390        DefaultConfiguration typeConf = new DefaultConfiguration("contentTypes");
391        typeConf.setValue(cType.getId());
392        classConf.addChild(typeConf);
393        
394        // Icons or glyph
395        if (cType.getIconGlyph() != null)
396        {
397            DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph");
398            iconGlyphConf.setValue(cType.getIconGlyph());
399            classConf.addChild(iconGlyphConf);
400        }
401        if (cType.getIconDecorator() != null)
402        {
403            DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator");
404            iconDecoratorConf.setValue(cType.getIconDecorator());
405            classConf.addChild(iconDecoratorConf);
406        }
407        if (cType.getSmallIcon() != null)
408        {
409            DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
410            iconSmallConf.setValue(cType.getSmallIcon());
411            classConf.addChild(iconSmallConf);
412            DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
413            iconMediumConf.setValue(cType.getMediumIcon());
414            classConf.addChild(iconMediumConf);
415            DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
416            iconLargeConf.setValue(cType.getLargeIcon());
417            classConf.addChild(iconLargeConf);
418        }
419        
420        // Common configuration
421        @SuppressWarnings("unchecked")
422        Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("items-config");
423        for (String tagName : commonConfig.keySet())
424        {
425            DefaultConfiguration c = new DefaultConfiguration(tagName);
426            c.setValue(String.valueOf(commonConfig.get(tagName)));
427            classConf.addChild(c);
428        }
429        
430        rootConf.addChild(classConf);
431        
432        _addRightsOnContentTypeConfiguration(rootConf);
433    }
434    
435    /**
436     * Get the 'rights' configuration of the content type item
437     * @param rootConf The root configuration
438     */
439    protected void _addRightsOnContentTypeConfiguration(DefaultConfiguration rootConf)
440    {
441        DefaultConfiguration rightsConf = new DefaultConfiguration("rights");
442        rightsConf.setAttribute("mode", _rightsMode);
443        for (String rightId : _rights.keySet())
444        {
445            DefaultConfiguration rightConf = new DefaultConfiguration("right");
446            Optional.ofNullable(_rights.get(rightId)).ifPresent(contextPrefix -> rightConf.setAttribute("context-prefix", contextPrefix));
447            rightConf.setValue(rightId);
448            rightsConf.addChild(rightConf);
449        }
450        rootConf.addChild(rightsConf);
451    }
452    
453    /**
454     * Get the list of content types classified by groups
455     * @return The content types
456     */
457    protected Map<I18nizableText, Set<ContentType>> _getContentTypesByGroup ()
458    {
459        Map<I18nizableText, Set<ContentType>> groups = new TreeMap<>(new I18nizableTextKeyComparator());
460        
461        if (this._script.getParameters().get("contentTypes") != null)
462        {
463            String[] contentTypesIds = ((String) this._script.getParameters().get("contentTypes")).split(",");
464            
465            boolean allowInherit = "true".equals(this._script.getParameters().get("allowInherit"));
466            
467            for (String contentTypeId : contentTypesIds)
468            {
469                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
470                
471                if (isValidContentType(contentType))
472                {
473                    addContentType (contentType, groups);
474                }
475                
476                if (allowInherit)
477                {
478                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(contentTypeId))
479                    {
480                        ContentType subContentType = _contentTypeExtensionPoint.getExtension(subTypeId);
481                        if (isValidContentType(subContentType))
482                        {
483                            addContentType (subContentType, groups);
484                        }
485                    }
486                }
487            }
488        }
489        else
490        {
491            Set<String> contentTypesIds = _contentTypeExtensionPoint.getExtensionsIds();
492            
493            for (String contentTypeId : contentTypesIds)
494            {
495                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
496                
497                if (isValidContentType(contentType))
498                {
499                    addContentType (contentType, groups);
500                }
501            }
502        }
503        
504        return groups;
505    }
506    
507    /**
508     * Add content to groups
509     * @param contentType The content type
510     * @param groups The groups
511     */
512    protected void addContentType (ContentType contentType, Map<I18nizableText, Set<ContentType>> groups)
513    {
514        I18nizableText group = contentType.getCategory();
515        if ((group.isI18n() && group.getKey().isEmpty()) || (!group.isI18n() && group.getLabel().isEmpty()))
516        {
517            group = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT");
518        }
519        
520        if (!groups.containsKey(group))
521        {
522            groups.put(group, new TreeSet<>(new ContentTypeComparator()));
523        }
524        Set<ContentType> cTypes = groups.get(group);
525        cTypes.add(contentType);
526    }
527    
528    @Override
529    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
530    {
531        if (_showInMenu())
532        {
533            // In this mode, the available content types will be displayed into menu items, classified by category (sub menu items)
534            try
535            {
536                _lazyInitializeContentTypeMenu();
537            }
538            catch (Exception e)
539            {
540                throw new IllegalStateException("Unable to lookup client side element local components", e);
541            }
542        }
543        
544        for (String id: _contentTypeExtensionPoint.getExtensionsIds())
545        {
546            ContentType contentType = _contentTypeExtensionPoint.getExtension(id);
547            if (isValidContentType(contentType))
548            {
549                return super.getScripts(ignoreRights, contextParameters);
550            }
551        }
552        return new ArrayList<>();
553    }
554    
555    /**
556     * Determines if the content type is a valid content type for the gallery
557     * @param contentType The coentent
558     * @return true if it is a valid content type
559     */
560    protected boolean isValidContentType (ContentType contentType)
561    {
562        return !contentType.isAbstract() && !contentType.isPrivate() && !contentType.isMixin();
563    }
564    
565    /**
566     * Test if the current user has the right needed by the content type to create a content.
567     * @param cType the content type
568     * @return true if the user has the right needed, false otherwise.
569     */
570    protected boolean hasRight(ContentType cType)
571    {
572        String right = cType.getRight();
573        
574        if (right == null)
575        {
576            return true;
577        }
578        else
579        {
580            UserIdentity user = _currentUserProvider.getUser();
581            return _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
582        }
583    }
584    
585    /**
586     * Comparator used to order the content types in the categories
587     * We use the translated labels to make it easier to find a content type
588     * But the order will be different for each language
589     */
590    class ContentTypeComparator implements Comparator<ContentType>
591    {
592        @Override
593        public int compare(ContentType c1, ContentType c2)
594        {
595            if (c1 == c2)
596            {
597                return 0;
598            }
599            
600            I18nizableText t1 = c1.getLabel();
601            I18nizableText t2 = c2.getLabel();
602            
603            String str1 = _i18nUtils.translate(t1);
604            if (str1 == null)
605            {
606                str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
607            }
608            String str2 = _i18nUtils.translate(t2);
609            if (str2 == null)
610            {
611                str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
612            }
613            
614            int compareTo = str1.toLowerCase().compareTo(str2.toLowerCase());
615            if (compareTo == 0)
616            {
617                // Content types could have same labels but there are not equals, so do not return 0 to add it in TreeSet
618                // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
619                return 1;
620            }
621            return compareTo;
622        }
623    }
624}