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