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.stream.Collectors;
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 sorted by alphabetical order
135                        List<ClientSideElement> 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);
144                            }
145                        }
146                        
147                        if (gpItems.size() > 0)
148                        {
149                            List<String> gpItemIds = gpItems.stream().map(e -> e.getId()).collect(Collectors.toList());
150                            groupParams.put("items", gpItemIds);
151                            galleryGroups.add(groupParams);
152                        }
153                    }
154                }
155            }
156        }
157    }
158    
159    private boolean _showInMenu()
160    {
161        return _script.getParameters().containsKey("show-in-menu") && _script.getParameters().get("show-in-menu").equals("true");
162    }
163    
164    private synchronized void _lazyInitializeContentTypeGallery() throws ConfigurationException
165    {
166        if (!_contentTypesInitialized)
167        {
168            _addedComponents = new HashSet<>();
169            
170            Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup();
171            
172            if (cTypesByGroup.size() > 0)
173            {
174                GalleryItem galleryItem = new GalleryItem();
175                
176                for (I18nizableText groupLabel : cTypesByGroup.keySet())
177                {
178                    GalleryGroup galleryGroup = new GalleryGroup(groupLabel);
179                    galleryItem.addGroup(galleryGroup);
180                    
181                    Set<ContentType> cTypes = cTypesByGroup.get(groupLabel);
182                    for (ContentType cType : cTypes)
183                    {
184                        String id = this.getId() + "-" + cType.getId();
185                        
186                        DefaultConfiguration conf = new DefaultConfiguration("extension");
187                        conf.setAttribute("id", id);
188                        _addContentTypeConfiguration (conf, cType);
189                        
190                        if (!_addedComponents.contains(id))
191                        {
192                            _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
193                            galleryGroup.addItem(new UnresolvedItem(id, true));
194                            _addedComponents.add(id);
195                        }
196                    }
197                }
198                
199                _galleryItems.add(galleryItem);
200            }
201            
202            if (_galleryItems.size() > 0)
203            {
204                try
205                {
206                    _getGalleryItemManager().initialize();
207                }
208                catch (Exception e)
209                {
210                    throw new ConfigurationException("Unable to lookup parameter local components", e);
211                }
212            }
213        }
214        
215        _contentTypesInitialized = true;
216    }
217    
218    private synchronized void _lazyInitializeContentTypeMenu()
219    {
220        if (!_contentTypesInitialized)
221        {
222            _addedComponents = new HashSet<>();
223            
224            Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup();
225            
226            if (cTypesByGroup.size() > 0)
227            {
228                for (I18nizableText groupLabel : cTypesByGroup.keySet())
229                {
230                    String id = this.getId() + "-" + org.ametys.core.util.StringUtils.generateKey();
231                    DefaultConfiguration conf = new DefaultConfiguration("extension");
232                    conf.setAttribute("id", id);
233                    
234                    Set<ContentType> cTypes = cTypesByGroup.get(groupLabel);
235                    _addGroupContentTypeConfiguration (conf, groupLabel, cTypes);
236                    
237                    if (!_addedComponents.contains(id))
238                    {
239                        _menuItemManager.addComponent(_pluginName, null, id, SimpleMenu.class, conf);
240                        _unresolvedMenuItems.add(new UnresolvedItem(id, true));
241                        _addedComponents.add(id);
242                    }
243                }
244            }
245        }
246        
247        _contentTypesInitialized = true;
248    }
249    
250    /**
251     * Get the configuration of the content type item
252     * @param rootConf The root configuration
253     * @param groupLabel The group's label
254     * @param cTypes The content types of the group
255     */
256    protected void _addGroupContentTypeConfiguration (DefaultConfiguration rootConf, I18nizableText groupLabel, Set<ContentType> cTypes)
257    {
258        DefaultConfiguration classConf = new DefaultConfiguration("class");
259        classConf.setAttribute("name", "Ametys.plugins.cms.content.controller.ContentTypeMenuItemController");
260        
261        // Label and description
262        DefaultConfiguration labelConf = new DefaultConfiguration("label");
263        DefaultConfiguration descConf = new DefaultConfiguration("description");
264        if (groupLabel.isI18n())
265        {
266            labelConf.setAttribute("i18n", "true");
267            labelConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey());
268            descConf.setAttribute("i18n", "true");
269            descConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey());
270        }
271        else
272        {
273            labelConf.setValue(groupLabel.getLabel());
274            descConf.setValue(groupLabel.getLabel());
275        }
276        classConf.addChild(labelConf);
277        classConf.addChild(descConf);
278        
279        // Icons or glyph (use the first content type of the list)
280        ContentType firstCtype = cTypes.iterator().next();
281        if (firstCtype.getIconGlyph() != null)
282        {
283            DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph");
284            iconGlyphConf.setValue(firstCtype.getIconGlyph());
285            classConf.addChild(iconGlyphConf);
286        }
287        if (firstCtype.getIconDecorator() != null)
288        {
289            DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator");
290            iconDecoratorConf.setValue(firstCtype.getIconDecorator());
291            classConf.addChild(iconDecoratorConf);
292        }
293        if (firstCtype.getSmallIcon() != null)
294        {
295            DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
296            iconSmallConf.setValue(firstCtype.getSmallIcon());
297            classConf.addChild(iconSmallConf);
298            DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
299            iconMediumConf.setValue(firstCtype.getMediumIcon());
300            classConf.addChild(iconMediumConf);
301            DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
302            iconLargeConf.setValue(firstCtype.getLargeIcon());
303            classConf.addChild(iconLargeConf);
304        }
305        
306        // Common configuration
307        @SuppressWarnings("unchecked")
308        Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("group-items-config");
309        for (String tagName : commonConfig.keySet())
310        {
311            DefaultConfiguration c = new DefaultConfiguration(tagName);
312            c.setValue(String.valueOf(commonConfig.get(tagName)));
313            classConf.addChild(c);
314        }
315        
316        rootConf.addChild(classConf);
317        
318        // Menu items
319        DefaultConfiguration menuItemsConf = new DefaultConfiguration("menu-items");
320        
321        for (ContentType contentType : cTypes)
322        {
323            DefaultConfiguration menuItemConf = new DefaultConfiguration("item");
324            menuItemConf.setAttribute("id", this.getId() + "-" + contentType.getId());
325            _addContentTypeConfiguration (menuItemConf, contentType);
326            
327            menuItemsConf.addChild(menuItemConf);
328        }
329        
330        rootConf.addChild(menuItemsConf);
331    }
332    
333    /**
334     * Get the configuration of the content type item
335     * @param rootConf The root configuration
336     * @param cType The content type
337     */
338    protected void _addContentTypeConfiguration (DefaultConfiguration rootConf, ContentType cType)
339    {
340        DefaultConfiguration classConf = new DefaultConfiguration("class");
341        classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController");
342        
343        // enable toggle
344        DefaultConfiguration enableToggleConf = new DefaultConfiguration("toggle-enabled");
345        enableToggleConf.setValue("true");
346        classConf.addChild(enableToggleConf);
347
348        // Parent controller id
349        DefaultConfiguration parentIdConf = new DefaultConfiguration("controllerParentId");
350        parentIdConf.setValue(this.getId());
351        classConf.addChild(parentIdConf);
352        
353        // Label
354        DefaultConfiguration labelConf = _getI18nizableTextConfiguration("label", cType.getLabel());
355        classConf.addChild(labelConf);
356        
357        // Description
358        DefaultConfiguration descConf = _getI18nizableTextConfiguration("description", cType.getDescription());
359        classConf.addChild(descConf);
360        
361        // Default content title
362        DefaultConfiguration defaultTitleConf = _getI18nizableTextConfiguration("defaultContentTitle", cType.getDefaultTitle());
363        classConf.addChild(defaultTitleConf);
364        
365        // Content type
366        DefaultConfiguration typeConf = new DefaultConfiguration("contentTypes");
367        typeConf.setValue(cType.getId());
368        classConf.addChild(typeConf);
369        
370        // Icons or glyph
371        _addContentTypeIconsConfiguration(classConf, cType);
372        
373        // Common configuration
374        @SuppressWarnings("unchecked")
375        Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("items-config");
376        for (String tagName : commonConfig.keySet())
377        {
378            DefaultConfiguration c = new DefaultConfiguration(tagName);
379            c.setValue(String.valueOf(commonConfig.get(tagName)));
380            classConf.addChild(c);
381        }
382        
383        rootConf.addChild(classConf);
384        
385        _addRightsOnContentTypeConfiguration(rootConf);
386    }
387    
388    /**
389     * Get the configuration for an i18nizable text
390     * @param tagName the tag name
391     * @param i18nText the i18n text
392     * @return the configuration for i18nizable text
393     */
394    protected DefaultConfiguration _getI18nizableTextConfiguration(String tagName, I18nizableText i18nText)
395    {
396        DefaultConfiguration i18nConf = new DefaultConfiguration(tagName);
397        if (i18nText.isI18n())
398        {
399            i18nConf.setAttribute("i18n", "true");
400            i18nConf.setValue(i18nText.getCatalogue() + ":" + i18nText.getKey());
401        }
402        else
403        {
404            i18nConf.setValue(i18nText.getLabel());
405        }
406        return i18nConf;
407    }
408    
409    /**
410     * Get the 'rights' configuration of the content type item
411     * @param rootConf The root configuration
412     */
413    protected void _addRightsOnContentTypeConfiguration(DefaultConfiguration rootConf)
414    {
415        DefaultConfiguration rightsConf = new DefaultConfiguration("rights");
416        rightsConf.setAttribute("mode", _rightsMode);
417        for (String rightId : _rights.keySet())
418        {
419            DefaultConfiguration rightConf = new DefaultConfiguration("right");
420            Optional.ofNullable(_rights.get(rightId)).ifPresent(contextPrefix -> rightConf.setAttribute("context-prefix", contextPrefix));
421            rightConf.setValue(rightId);
422            rightsConf.addChild(rightConf);
423        }
424        rootConf.addChild(rightsConf);
425    }
426    
427    /**
428     * Add configuration for content type's icons and/or glyphes
429     * @param classConf the class configuration
430     * @param cType the content type
431     */
432    protected void _addContentTypeIconsConfiguration(DefaultConfiguration classConf, ContentType cType)
433    {
434        if (cType.getIconGlyph() != null)
435        {
436            DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph");
437            iconGlyphConf.setValue(cType.getIconGlyph());
438            classConf.addChild(iconGlyphConf);
439        }
440        if (cType.getIconDecorator() != null)
441        {
442            DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator");
443            iconDecoratorConf.setValue(cType.getIconDecorator());
444            classConf.addChild(iconDecoratorConf);
445        }
446        if (cType.getSmallIcon() != null)
447        {
448            DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
449            iconSmallConf.setValue(cType.getSmallIcon());
450            classConf.addChild(iconSmallConf);
451            DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
452            iconMediumConf.setValue(cType.getMediumIcon());
453            classConf.addChild(iconMediumConf);
454            DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
455            iconLargeConf.setValue(cType.getLargeIcon());
456            classConf.addChild(iconLargeConf);
457        }
458    }
459    
460    /**
461     * Get the list of content types classified by groups
462     * @return The content types
463     */
464    protected Map<I18nizableText, Set<ContentType>> _getContentTypesByGroup ()
465    {
466        Map<I18nizableText, Set<ContentType>> groups = new TreeMap<>(new I18nizableTextKeyComparator());
467        
468        if (this._script.getParameters().get("contentTypes") != null)
469        {
470            String[] contentTypesIds = ((String) this._script.getParameters().get("contentTypes")).split(",");
471            
472            boolean allowInherit = "true".equals(this._script.getParameters().get("allowInherit"));
473            
474            for (String contentTypeId : contentTypesIds)
475            {
476                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
477                
478                if (isValidContentType(contentType))
479                {
480                    addContentType (contentType, groups);
481                }
482                
483                if (allowInherit)
484                {
485                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(contentTypeId))
486                    {
487                        ContentType subContentType = _contentTypeExtensionPoint.getExtension(subTypeId);
488                        if (isValidContentType(subContentType))
489                        {
490                            addContentType (subContentType, groups);
491                        }
492                    }
493                }
494            }
495        }
496        else
497        {
498            Set<String> contentTypesIds = _contentTypeExtensionPoint.getExtensionsIds();
499            
500            for (String contentTypeId : contentTypesIds)
501            {
502                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
503                
504                if (isValidContentType(contentType))
505                {
506                    addContentType (contentType, groups);
507                }
508            }
509        }
510        
511        return groups;
512    }
513    
514    /**
515     * Add content to groups
516     * @param contentType The content type
517     * @param groups The groups
518     */
519    protected void addContentType (ContentType contentType, Map<I18nizableText, Set<ContentType>> groups)
520    {
521        I18nizableText group = contentType.getCategory();
522        if ((group.isI18n() && group.getKey().isEmpty()) || (!group.isI18n() && group.getLabel().isEmpty()))
523        {
524            group = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT");
525        }
526        
527        if (!groups.containsKey(group))
528        {
529            groups.put(group, new HashSet<>());
530        }
531        Set<ContentType> cTypes = groups.get(group);
532        cTypes.add(contentType);
533    }
534    
535    @Override
536    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
537    {
538        if (_showInMenu())
539        {
540            // In this mode, the available content types will be displayed into menu items, classified by category (sub menu items)
541            try
542            {
543                _lazyInitializeContentTypeMenu();
544            }
545            catch (Exception e)
546            {
547                throw new IllegalStateException("Unable to lookup client side element local components", e);
548            }
549        }
550        
551        for (String id: _contentTypeExtensionPoint.getExtensionsIds())
552        {
553            ContentType contentType = _contentTypeExtensionPoint.getExtension(id);
554            if (isValidContentType(contentType))
555            {
556                return super.getScripts(ignoreRights, contextParameters);
557            }
558        }
559        return new ArrayList<>();
560    }
561    
562    /**
563     * Determines if the content type is a valid content type for the gallery
564     * @param contentType The coentent
565     * @return true if it is a valid content type
566     */
567    protected boolean isValidContentType (ContentType contentType)
568    {
569        return !contentType.isAbstract() && !contentType.isPrivate() && !contentType.isMixin() && !contentType.isReferenceTable();
570    }
571    
572    /**
573     * Test if the current user has the right needed by the content type to create a content.
574     * @param cType the content type
575     * @return true if the user has the right needed, false otherwise.
576     */
577    protected boolean hasRight(ContentType cType)
578    {
579        String right = cType.getRight();
580        
581        if (right == null)
582        {
583            return true;
584        }
585        else
586        {
587            UserIdentity user = _currentUserProvider.getUser();
588            return _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
589        }
590    }
591    
592    /**
593     * Comparator used to order the content types in the categories
594     * We use the translated labels to make it easier to find a content type
595     * But the order will be different for each language
596     */
597    class ContentTypeClientSideElementComparator implements Comparator<ClientSideElement>
598    {
599        String _parentMenuId;
600        
601        public ContentTypeClientSideElementComparator(String parentMenuId)
602        {
603            _parentMenuId = parentMenuId;
604        }
605        
606        @Override
607        public int compare(ClientSideElement c1, ClientSideElement c2)
608        {
609            if (c1 == c2)
610            {
611                return 0;
612            }
613            
614            String cTypeId1 = c1.getId().substring(_parentMenuId.length() + 1);
615            ContentType cType1 = _contentTypeExtensionPoint.getExtension(cTypeId1);
616            I18nizableText t1 = cType1.getLabel();
617            
618            String cTypeId2 = c2.getId().substring(_parentMenuId.length() + 1);
619            ContentType cType2 = _contentTypeExtensionPoint.getExtension(cTypeId2);
620            I18nizableText t2 = cType2.getLabel();
621            
622            String str1 = _i18nUtils.translate(t1);
623            if (str1 == null)
624            {
625                str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
626            }
627            String str2 = _i18nUtils.translate(t2);
628            if (str2 == null)
629            {
630                str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
631            }
632            
633            int compareTo = str1.toLowerCase().compareTo(str2.toLowerCase());
634            if (compareTo == 0)
635            {
636                // Content types could have same labels but there are not equals, so do not return 0 to add it in TreeSet
637                // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
638                return 1;
639            }
640            return compareTo;
641        }
642    }
643}