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