001/*
002 *  Copyright 2013 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.core.ui;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Map.Entry;
024
025import org.apache.avalon.framework.component.ComponentException;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfiguration;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.commons.lang3.StringUtils;
032import org.slf4j.LoggerFactory;
033
034import org.ametys.runtime.i18n.I18nizableText;
035import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
036
037/**
038 * This element creates a control button with a menu
039 */
040public class SimpleMenu extends StaticClientSideElement implements MenuClientSideElement
041{
042    /** The client side element component manager for menu items. */
043    protected ThreadSafeComponentManager<ClientSideElement> _menuItemManager;
044    /** The ribbon control manager */
045    protected RibbonControlsManager _ribbonControlManager;
046    /** The service manager */
047    protected ServiceManager _smanager;
048    
049    /** The menu items */
050    protected List<ClientSideElement> _menuItems;
051    /** The gallery items */
052    protected List<GalleryItem> _galleryItems;
053    /** The primary menu item */
054    protected ClientSideElement _primaryMenuItem;
055    /** The unresolved menu items */
056    protected List<UnresolvedItem> _unresolvedMenuItems;
057    
058    /** The client side element component manager for gallery items. */
059    private ThreadSafeComponentManager<ClientSideElement> _galleryItemManager;
060    
061    @Override
062    public void service(ServiceManager smanager) throws ServiceException
063    {
064        super.service(smanager);
065        _smanager = smanager;
066        _ribbonControlManager = (RibbonControlsManager) smanager.lookup(RibbonControlsManager.ROLE);
067    }
068    
069    @Override
070    public void configure(Configuration configuration) throws ConfigurationException
071    {
072        _menuItemManager = new ThreadSafeComponentManager<>();
073        _menuItemManager.setLogger(LoggerFactory.getLogger("cms.plugin.threadsafecomponent"));
074        _menuItemManager.service(_smanager);
075        
076        _initializeGalleryItemManager();
077        
078        super.configure(configuration);
079        
080        _galleryItems = new ArrayList<>();
081        
082        _menuItems = new ArrayList<>();
083        _unresolvedMenuItems = new ArrayList<>();
084        
085        _configureGalleries (configuration);
086        _configureItemsMenu(configuration);
087    }
088    
089    @Override
090    protected String _configureClass(Configuration configuration) throws ConfigurationException
091    {
092        String jsClassName = configuration.getAttribute("name", "");
093        if (StringUtils.isNotEmpty(jsClassName))
094        {
095            if (getLogger().isDebugEnabled())
096            {
097                getLogger().debug("Js class configured is '" + jsClassName + "'");
098            }
099        }
100        return jsClassName;   
101    }
102    
103    /**
104     * Initialize the gallery item manager
105     */
106    protected void _initializeGalleryItemManager()
107    {
108        if (_galleryItemManager != null)
109        {
110            _galleryItemManager.dispose();
111        }
112        
113        _galleryItemManager = new ThreadSafeComponentManager<>();
114        _galleryItemManager.setLogger(LoggerFactory.getLogger("cms.plugin.threadsafecomponent"));
115        _galleryItemManager.service(_smanager);
116    }
117    
118    
119    /**
120     * Get the gallery item manager
121     * @return the gallery item manager
122     */
123    protected ThreadSafeComponentManager<ClientSideElement> _getGalleryItemManager()
124    {
125        return _galleryItemManager;
126    }
127
128    /**
129     * Get the gallery items
130     * @return the gallery items
131     */
132    protected List<GalleryItem> _getGalleryItems()
133    {
134        return _galleryItems;
135    }
136
137    @Override
138    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
139    {
140        try
141        {
142            // ensure all menu items have been resolved
143            _resolveMenuItems();
144            
145            // ensure all gallery items have been resolved
146            _resolveGalleryItems();
147        }
148        catch (Exception e)
149        {
150            throw new IllegalStateException("Unable to lookup client side element local components", e);
151        }
152        
153        // FIXME handle rights for workspace admin, here is a temporary workaround
154        if (ignoreRights || hasRight(getRights(contextParameters)))
155        {
156            List<Script> scripts = super.getScripts(ignoreRights, contextParameters);
157            Map<String, Object> parameters = new HashMap<>();
158            List<ScriptFile> cssFiles = new ArrayList<>();
159            List<ScriptFile> scriptFiles = new ArrayList<>();
160            for (Script script : scripts)
161            {
162                cssFiles.addAll(script.getCSSFiles());
163                scriptFiles.addAll(script.getScriptFiles());
164                parameters.putAll(script.getParameters());
165            }
166            
167            String scriptClassName = _script.getScriptClassname();
168            
169            if (_primaryMenuItem != null)
170            {
171                List<Script> itemScripts = _primaryMenuItem.getScripts(ignoreRights, contextParameters);
172                for (Script script : itemScripts)
173                {
174                    Map<String, Object> primaryParameters = script.getParameters();
175                    parameters.put("primary-menu-item-id", script.getId());
176                    for (String paramId : primaryParameters.keySet())
177                    {
178                        if (!parameters.containsKey(paramId))
179                        {
180                            parameters.put(paramId, primaryParameters.get(paramId));
181                        }
182                    }
183                    
184                    if (StringUtils.isEmpty(scriptClassName))
185                    {
186                        scriptClassName = script.getScriptClassname();
187                    }
188                }
189            }
190            
191            if (StringUtils.isNotBlank(scriptClassName))
192            {
193                // Gallery items
194                _getGalleryItems(parameters, contextParameters);
195    
196                // Menu items
197                _getMenuItems(parameters, contextParameters);
198                
199                List<Script> result = new ArrayList<>();
200                result.add(new Script(this.getId(), scriptClassName, scriptFiles, cssFiles, parameters));
201                return result;
202            }
203        }
204        
205        return new ArrayList<>();
206    }
207    
208    @Override
209    public Map<String, String> getRights(Map<String, Object> contextParameters)
210    {
211        Map<String, String> rights = super.getRights(contextParameters);
212        
213        if (rights.size() == 0 && _primaryMenuItem != null)
214        {
215            return _primaryMenuItem.getRights(contextParameters);
216        }
217        
218        return rights;
219    }
220    
221    /**
222     * Get the gallery items
223     * @param parameters Contextual the parameters given to the control script class.
224     * @param contextualParameters Contextual parameters transmitted by the environment.
225     */
226    @SuppressWarnings("unchecked")
227    protected void _getGalleryItems (Map<String, Object> parameters, Map<String, Object> contextualParameters)
228    {
229        List<GalleryItem> galleryItems = _getGalleryItems();
230        
231        if (galleryItems.size() > 0)
232        {
233            parameters.put("gallery-item", new LinkedHashMap<>());
234            
235            for (GalleryItem galleryItem : galleryItems)
236            {
237                Map<String, Object> galleryItemsParams = (Map<String, Object>) parameters.get("gallery-item");
238                galleryItemsParams.put("gallery-groups", new ArrayList<>());
239                
240                for (GalleryGroup galleryGp : galleryItem.getGroups())
241                {
242                    List<Object> galleryGroups = (List<Object>) galleryItemsParams.get("gallery-groups");
243                    
244                    Map<String, Object> groupParams = new LinkedHashMap<>();
245                    
246                    I18nizableText label = galleryGp.getLabel();
247                    groupParams.put("label", label);
248                    
249                    // Group items
250                    List<String> gpItems = new ArrayList<>();
251                    for (ClientSideElement element : galleryGp.getItems())
252                    {
253                        gpItems.add(element.getId());
254                    }
255                    groupParams.put("items", gpItems);
256                    
257                    galleryGroups.add(groupParams);
258                }
259            }
260        }
261    }
262    
263    /**
264     * Get the menu items
265     * @param parameters Contextual the parameters given to the control script class.
266     * @param contextualParameters Contextual parameters transmitted by the environment.
267     */
268    protected void _getMenuItems (Map<String, Object> parameters, Map<String, Object> contextualParameters)
269    {
270        if (_menuItems.size() > 0)
271        {
272            List<String> menuItems = new ArrayList<>();
273            for (ClientSideElement element : _menuItems)
274            {
275                menuItems.add(element.getId());
276            }
277            parameters.put("menu-items", menuItems);
278        }
279    }
280    
281    @Override
282    public List<ClientSideElement> getReferencedClientSideElements(Map<String, Object> contextParameters)
283    {
284        List<ClientSideElement> result = new ArrayList<>();
285        
286        if (hasRight(getRights(contextParameters)))
287        {
288            result.addAll(_menuItems);
289            
290            _getGalleryItems().stream()
291                              .map(GalleryItem::getGroups)
292                              .flatMap(List::stream)
293                              .map(GalleryGroup::getItems)
294                              .forEach(result::addAll);
295        }
296        
297        return result;
298    }
299    
300    /**
301     * Configure the galleries
302     * @param configuration the configuration
303     * @throws ConfigurationException If the configuration has an issue
304     */
305    protected void _configureGalleries(Configuration configuration) throws ConfigurationException
306    {
307        for (Configuration galleryConfiguration : configuration.getChildren("gallery-item"))
308        {
309            GalleryItem galleryItem = new GalleryItem();
310            
311            for (Configuration gpConfiguration : galleryConfiguration.getChildren("gallery-group"))
312            {
313                galleryItem.addGroup(_configureGroupGallery(gpConfiguration));
314            }
315            
316            _galleryItems.add(galleryItem);
317        }
318        
319        // FIXME
320        if (_galleryItems.size() > 0)
321        {
322            try
323            {
324                _getGalleryItemManager().initialize();
325            }
326            catch (Exception e)
327            {
328                throw new ConfigurationException("Unable to lookup parameter local components", configuration, e);
329            }
330        }
331    }
332    
333    /**
334     * Configure a group gallery
335     * @param configuration the configuration
336     * @return The configured group gallery
337     * @throws ConfigurationException If the configuration has an issue
338     */
339    protected GalleryGroup _configureGroupGallery(Configuration configuration) throws ConfigurationException
340    {
341        I18nizableText label;
342        
343        Configuration labelConfig = configuration.getChild("label");
344        if (labelConfig.getAttributeAsBoolean("i18n", false))
345        {
346            label = new I18nizableText("plugin." + _pluginName, labelConfig.getValue(""));
347        }
348        else
349        {
350            label = new I18nizableText(labelConfig.getValue(""));
351        }
352        
353        GalleryGroup galleryGroup = new GalleryGroup(label);
354        
355        for (Configuration itemConfig : configuration.getChildren("item"))
356        {
357            if (itemConfig.getAttribute("ref", null) != null)
358            {
359                galleryGroup.addItem(new UnresolvedItem(itemConfig.getAttribute("ref"), false));
360            }
361            else
362            {
363                String id = itemConfig.getAttribute("id");
364                DefaultConfiguration conf = new DefaultConfiguration("extension");
365                conf.setAttribute("id", id);
366                conf.addChild(itemConfig.getChild("class"));
367                
368                Map<String, List<String>> childDependencies = _configureDependencies(itemConfig);
369                _addDependencies(childDependencies);
370                
371                if (itemConfig.getChild("right", false) != null)
372                {
373                    conf.addChild(itemConfig.getChild("right"));
374                }
375                if (itemConfig.getChild("rights", false) != null)
376                {
377                    conf.addChild(itemConfig.getChild("rights"));
378                }
379                
380                _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
381                
382                galleryGroup.addItem(new UnresolvedItem(id, true));
383            }
384        }
385        
386        return galleryGroup;
387    }
388    
389    
390    /**
391     * Configure the items menu
392     * @param configuration the configuration
393     * @throws ConfigurationException If the configuration has an issue
394     */
395    protected void _configureItemsMenu(Configuration configuration) throws ConfigurationException
396    {
397        for (Configuration menuItemConfiguration : configuration.getChildren("menu-items"))
398        {
399            for (Configuration itemConfig : menuItemConfiguration.getChildren("item"))
400            {
401                boolean isPrimary = itemConfig.getAttributeAsBoolean("primaryItem", false);
402                
403                if (itemConfig.getAttribute("ref", null) != null)
404                {
405                    _unresolvedMenuItems.add(new UnresolvedItem(itemConfig.getAttribute("ref"), false, isPrimary));
406                }
407                else
408                {
409                    String id = itemConfig.getAttribute("id");
410                    DefaultConfiguration conf = new DefaultConfiguration("extension");
411                    conf.setAttribute("id", id);
412                    conf.addChild(itemConfig.getChild("class"));
413                    
414                    Map<String, List<String>> childDependencies = _configureDependencies(itemConfig);
415                    _addDependencies(childDependencies);
416                    
417                    if (itemConfig.getChild("menu-items", false) != null || itemConfig.getChild("gallery-item", false) != null)
418                    {
419                        if (itemConfig.getChild("menu-items", false) != null)
420                        {
421                            conf.addChild(itemConfig.getChild("menu-items"));
422                        }
423                        
424                        if (itemConfig.getChild("gallery-item", false) != null)
425                        {
426                            conf.addChild(itemConfig.getChild("gallery-item"));
427                        }
428                        
429                        _menuItemManager.addComponent(_pluginName, null, id, SimpleMenu.class, conf);
430                    }
431                    else
432                    {
433                        _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
434                    }
435                    
436                    _unresolvedMenuItems.add(new UnresolvedItem(id, true, isPrimary));
437                }
438            }
439        }        
440    }
441    
442    private void _resolveMenuItems () throws Exception
443    {
444        if (_unresolvedMenuItems != null)
445        {
446            _menuItemManager.initialize();
447            
448            for (UnresolvedItem unresolvedItem : _unresolvedMenuItems)
449            {
450                String id = unresolvedItem.getId();
451                ClientSideElement element;
452                if (unresolvedItem.isLocalItem())
453                {
454                    try
455                    {
456                        element = _menuItemManager.lookup(id);
457                    }
458                    catch (ComponentException e)
459                    {
460                        throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
461                    }
462                }
463                else
464                {
465                    element = _ribbonControlManager.getExtension(id);
466                }
467                
468                if (unresolvedItem.isPrimary())
469                {
470                    _primaryMenuItem = element;
471                }
472                
473                _menuItems.add(element);
474                
475                _addDependencies(element.getDependencies());
476            }
477        }
478        
479        _unresolvedMenuItems = null;
480    }
481    
482    private void _resolveGalleryItems()
483    {
484        for (GalleryItem item : _getGalleryItems())
485        {
486            for (GalleryGroup group : item.getGroups())
487            {
488                group.getItems();
489            }
490        }
491    }
492    
493    /**
494     * Add additional dependencies to the Menu, such as dependencies inherited from its menu items or gallery items.
495     * @param additionalDependencies The dependencies to add
496     * @throws ConfigurationException If an error occurs
497     */
498    protected void _addDependencies(Map<String, List<String>> additionalDependencies) throws ConfigurationException
499    {
500        if (!additionalDependencies.isEmpty())
501        {
502            for (Entry<String, List<String>> additionalDependency : additionalDependencies.entrySet())
503            {
504                String key = additionalDependency.getKey();
505                if (!_dependencies.containsKey(key))
506                {
507                    _dependencies.put(key, new ArrayList<>());
508                }
509                List<String> dependenciesList = _dependencies.get(key);
510                
511                for (String dependency : additionalDependency.getValue())
512                {
513                    if (!dependenciesList.contains(dependency))
514                    {
515                        dependenciesList.add(dependency);
516                    }
517                }
518            }
519        }
520    }
521    
522    /**
523     * Class representing a gallery item
524     *
525     */
526    public class GalleryItem
527    {
528        private final List<GalleryGroup> _groups;
529        
530        /**
531         * Constructor
532         */
533        public GalleryItem()
534        {
535            _groups = new ArrayList<>();
536        }
537        
538        /**
539         * Add a group of this gallery
540         * @param group The gallery group to add
541         */
542        public void addGroup (GalleryGroup group)
543        {
544            _groups.add(group);
545        }
546        
547        /**
548         * Get gallery's groups
549         * @return The gallery's group
550         */
551        public List<GalleryGroup> getGroups ()
552        {
553            return _groups;
554        }
555        
556    }
557    
558    /**
559     * Class representing a gallery group
560     *
561     */
562    public class GalleryGroup
563    {
564        private final I18nizableText _label;
565        private List<UnresolvedItem> _unresolvedGalleryItems;
566        private final List<ClientSideElement> _items;
567        
568        /**
569         * Constructor 
570         * @param label The group's label
571         */
572        public GalleryGroup(I18nizableText label)
573        {
574            _label = label;
575            _items = new ArrayList<>();
576            _unresolvedGalleryItems = new ArrayList<>();
577        }
578        
579        /**
580         * Add a new item to group
581         * @param item The item to add
582         */
583        public void addItem (UnresolvedItem item)
584        {
585            _unresolvedGalleryItems.add(item);
586        }
587        
588        /**
589         * Get the group's label
590         * @return The group's label
591         */
592        public I18nizableText getLabel ()
593        {
594            return _label;
595        }
596        
597        /**
598         * Get the gallery item
599         * @return The gallery item
600         */
601        public List<ClientSideElement> getItems ()
602        {
603            try
604            {
605                _resolveGalleryItems();
606            }
607            catch (Exception e)
608            {
609                throw new IllegalStateException("Unable to lookup client side element local components", e);
610            }
611            
612            return _items;
613        }
614        
615        private void _resolveGalleryItems () throws Exception
616        {
617            if (_unresolvedGalleryItems != null)
618            {
619                for (UnresolvedItem unresolvedItem : _unresolvedGalleryItems)
620                {
621                    ClientSideElement element;
622                    String id = unresolvedItem.getId();
623                    if (unresolvedItem.isLocalItem())
624                    {
625                        try
626                        {
627                            element = _getGalleryItemManager().lookup(id);
628                        }
629                        catch (ComponentException e)
630                        {
631                            throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
632                        }
633                    }
634                    else
635                    {
636                        element = _ribbonControlManager.getExtension(id);
637                    }
638                    
639                    _items.add(element);
640                }
641            }
642            
643            _unresolvedGalleryItems = null;
644        }
645    }
646    
647    /**
648     * The unresolved item
649     *
650     */
651    protected class UnresolvedItem
652    {
653        private final String _itemId;
654        private final boolean _local;
655        private final boolean _primary;
656        
657        /**
658         * Constructor
659         * @param id The item id
660         * @param local true if it is a local item
661         */
662        public UnresolvedItem(String id, boolean local)
663        {
664            _itemId = id;
665            _local = local;
666            _primary = false;
667        }
668        
669        /**
670         * Constructor
671         * @param id The item id
672         * @param local true if it is a local item
673         * @param primary true if it is a primary item
674         */
675        public UnresolvedItem(String id, boolean local, boolean primary)
676        {
677            _itemId = id;
678            _local = local;
679            _primary = primary;
680        }
681        
682        /**
683         * Get the item id
684         * @return the item id
685         */
686        public String getId ()
687        {
688            return _itemId;
689        }
690        
691        /**
692         * Return true if it is a local item
693         * @return true if it is a local item
694         */
695        public boolean isLocalItem ()
696        {
697            return _local;
698        }
699        
700        /**
701         * Return true if it is a primary item
702         * @return true if it is a primary item
703         */
704        public boolean isPrimary ()
705        {
706            return _primary;
707        }
708    }
709}