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()
270    {
271        List<ClientSideElement> result = new ArrayList<>();
272        
273        result.addAll(_menuItems);
274        
275        _getGalleryItems().stream()
276                          .map(GalleryItem::getGroups)
277                          .flatMap(List::stream)
278                          .map(GalleryGroup::getItems)
279                          .forEach(result::addAll);
280        
281        return result;
282    }
283    
284    /**
285     * Configure the galleries
286     * @param configuration the configuration
287     * @throws ConfigurationException If the configuration has an issue
288     */
289    protected void _configureGalleries(Configuration configuration) throws ConfigurationException
290    {
291        for (Configuration galleryConfiguration : configuration.getChildren("gallery-item"))
292        {
293            GalleryItem galleryItem = new GalleryItem();
294            
295            for (Configuration gpConfiguration : galleryConfiguration.getChildren("gallery-group"))
296            {
297                galleryItem.addGroup(_configureGroupGallery(gpConfiguration));
298            }
299            
300            _galleryItems.add(galleryItem);
301        }
302        
303        // FIXME
304        if (_galleryItems.size() > 0)
305        {
306            try
307            {
308                _getGalleryItemManager().initialize();
309            }
310            catch (Exception e)
311            {
312                throw new ConfigurationException("Unable to lookup parameter local components", configuration, e);
313            }
314        }
315    }
316    
317    /**
318     * Configure a group gallery
319     * @param configuration the configuration
320     * @return The configured group gallery
321     * @throws ConfigurationException If the configuration has an issue
322     */
323    protected GalleryGroup _configureGroupGallery(Configuration configuration) throws ConfigurationException
324    {
325        I18nizableText label;
326        
327        Configuration labelConfig = configuration.getChild("label");
328        if (labelConfig.getAttributeAsBoolean("i18n", false))
329        {
330            label = new I18nizableText("plugin." + _pluginName, labelConfig.getValue(""));
331        }
332        else
333        {
334            label = new I18nizableText(labelConfig.getValue(""));
335        }
336        
337        GalleryGroup galleryGroup = new GalleryGroup(label);
338        
339        for (Configuration itemConfig : configuration.getChildren("item"))
340        {
341            if (itemConfig.getAttribute("ref", null) != null)
342            {
343                galleryGroup.addItem(new UnresolvedItem(itemConfig.getAttribute("ref"), false));
344            }
345            else
346            {
347                String id = itemConfig.getAttribute("id");
348                DefaultConfiguration conf = new DefaultConfiguration("extension");
349                conf.setAttribute("id", id);
350                conf.addChild(itemConfig.getChild("class"));
351                
352                Map<String, List<String>> childDependencies = _configureDependencies(itemConfig);
353                _addDependencies(childDependencies);
354                
355                if (itemConfig.getChild("right", false) != null)
356                {
357                    conf.addChild(itemConfig.getChild("right"));
358                }
359                if (itemConfig.getChild("rights", false) != null)
360                {
361                    conf.addChild(itemConfig.getChild("rights"));
362                }
363                
364                _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
365                
366                galleryGroup.addItem(new UnresolvedItem(id, true));
367            }
368        }
369        
370        return galleryGroup;
371    }
372    
373    
374    /**
375     * Configure the items menu
376     * @param configuration the configuration
377     * @throws ConfigurationException If the configuration has an issue
378     */
379    protected void _configureItemsMenu(Configuration configuration) throws ConfigurationException
380    {
381        for (Configuration menuItemConfiguration : configuration.getChildren("menu-items"))
382        {
383            for (Configuration itemConfig : menuItemConfiguration.getChildren("item"))
384            {
385                boolean isPrimary = itemConfig.getAttributeAsBoolean("primaryItem", false);
386                
387                if (itemConfig.getAttribute("ref", null) != null)
388                {
389                    _unresolvedMenuItems.add(new UnresolvedItem(itemConfig.getAttribute("ref"), false, isPrimary));
390                }
391                else
392                {
393                    String id = itemConfig.getAttribute("id");
394                    DefaultConfiguration conf = new DefaultConfiguration("extension");
395                    conf.setAttribute("id", id);
396                    conf.addChild(itemConfig.getChild("class"));
397                    
398                    Map<String, List<String>> childDependencies = _configureDependencies(itemConfig);
399                    _addDependencies(childDependencies);
400                    
401                    if (itemConfig.getChild("menu-items", false) != null || itemConfig.getChild("gallery-item", false) != null)
402                    {
403                        if (itemConfig.getChild("menu-items", false) != null)
404                        {
405                            conf.addChild(itemConfig.getChild("menu-items"));
406                        }
407                        
408                        if (itemConfig.getChild("gallery-item", false) != null)
409                        {
410                            conf.addChild(itemConfig.getChild("gallery-item"));
411                        }
412                        
413                        _menuItemManager.addComponent(_pluginName, null, id, SimpleMenu.class, conf);
414                    }
415                    else
416                    {
417                        _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
418                    }
419                    
420                    _unresolvedMenuItems.add(new UnresolvedItem(id, true, isPrimary));
421                }
422            }
423        }        
424    }
425    
426    private void _resolveMenuItems () throws Exception
427    {
428        if (_unresolvedMenuItems != null)
429        {
430            _menuItemManager.initialize();
431            
432            for (UnresolvedItem unresolvedItem : _unresolvedMenuItems)
433            {
434                String id = unresolvedItem.getId();
435                ClientSideElement element;
436                if (unresolvedItem.isLocalItem())
437                {
438                    try
439                    {
440                        element = _menuItemManager.lookup(id);
441                    }
442                    catch (ComponentException e)
443                    {
444                        throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
445                    }
446                }
447                else
448                {
449                    element = _ribbonControlManager.getExtension(id);
450                }
451                
452                if (unresolvedItem.isPrimary())
453                {
454                    _primaryMenuItem = element;
455                }
456                
457                _menuItems.add(element);
458                
459                _addDependencies(element.getDependencies());
460            }
461        }
462        
463        _unresolvedMenuItems = null;
464    }
465    
466    private void _resolveGalleryItems()
467    {
468        for (GalleryItem item : _getGalleryItems())
469        {
470            for (GalleryGroup group : item.getGroups())
471            {
472                group.getItems();
473            }
474        }
475    }
476    
477    /**
478     * Add additional dependencies to the Menu, such as dependencies inherited from its menu items or gallery items.
479     * @param additionalDependencies The dependencies to add
480     * @throws ConfigurationException If an error occurs
481     */
482    protected void _addDependencies(Map<String, List<String>> additionalDependencies) throws ConfigurationException
483    {
484        if (!additionalDependencies.isEmpty())
485        {
486            for (Entry<String, List<String>> additionalDependency : additionalDependencies.entrySet())
487            {
488                String key = additionalDependency.getKey();
489                if (!_dependencies.containsKey(key))
490                {
491                    _dependencies.put(key, new ArrayList<String>());
492                }
493                List<String> dependenciesList = _dependencies.get(key);
494                
495                for (String dependency : additionalDependency.getValue())
496                {
497                    if (!dependenciesList.contains(dependency))
498                    {
499                        dependenciesList.add(dependency);
500                    }
501                }
502            }
503        }
504    }
505    
506    /**
507     * Class representing a gallery item
508     *
509     */
510    public class GalleryItem
511    {
512        private final List<GalleryGroup> _groups;
513        
514        /**
515         * Constructor
516         */
517        public GalleryItem()
518        {
519            _groups = new ArrayList<>();
520        }
521        
522        /**
523         * Add a group of this gallery
524         * @param group The gallery group to add
525         */
526        public void addGroup (GalleryGroup group)
527        {
528            _groups.add(group);
529        }
530        
531        /**
532         * Get gallery's groups
533         * @return The gallery's group
534         */
535        public List<GalleryGroup> getGroups ()
536        {
537            return _groups;
538        }
539        
540    }
541    
542    /**
543     * Class representing a gallery group
544     *
545     */
546    public class GalleryGroup
547    {
548        private final I18nizableText _label;
549        private List<UnresolvedItem> _unresolvedGalleryItems;
550        private final List<ClientSideElement> _items;
551        
552        /**
553         * Constructor 
554         * @param label The group's label
555         */
556        public GalleryGroup(I18nizableText label)
557        {
558            _label = label;
559            _items = new ArrayList<>();
560            _unresolvedGalleryItems = new ArrayList<>();
561        }
562        
563        /**
564         * Add a new item to group
565         * @param item The item to add
566         */
567        public void addItem (UnresolvedItem item)
568        {
569            _unresolvedGalleryItems.add(item);
570        }
571        
572        /**
573         * Get the group's label
574         * @return The group's label
575         */
576        public I18nizableText getLabel ()
577        {
578            return _label;
579        }
580        
581        /**
582         * Get the gallery item
583         * @return The gallery item
584         */
585        public List<ClientSideElement> getItems ()
586        {
587            try
588            {
589                _resolveGalleryItems();
590            }
591            catch (Exception e)
592            {
593                throw new IllegalStateException("Unable to lookup client side element local components", e);
594            }
595            
596            return _items;
597        }
598        
599        private void _resolveGalleryItems () throws Exception
600        {
601            if (_unresolvedGalleryItems != null)
602            {
603                for (UnresolvedItem unresolvedItem : _unresolvedGalleryItems)
604                {
605                    ClientSideElement element;
606                    String id = unresolvedItem.getId();
607                    if (unresolvedItem.isLocalItem())
608                    {
609                        try
610                        {
611                            element = _getGalleryItemManager().lookup(id);
612                        }
613                        catch (ComponentException e)
614                        {
615                            throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
616                        }
617                    }
618                    else
619                    {
620                        element = _ribbonControlManager.getExtension(id);
621                    }
622                    
623                    _items.add(element);
624                }
625            }
626            
627            _unresolvedGalleryItems = null;
628        }
629    }
630    
631    /**
632     * The unresolved item
633     *
634     */
635    protected class UnresolvedItem
636    {
637        private final String _itemId;
638        private final boolean _local;
639        private final boolean _primary;
640        
641        /**
642         * Constructor
643         * @param id The item id
644         * @param local true if it is a local item
645         */
646        public UnresolvedItem(String id, boolean local)
647        {
648            _itemId = id;
649            _local = local;
650            _primary = false;
651        }
652        
653        /**
654         * Constructor
655         * @param id The item id
656         * @param local true if it is a local item
657         * @param primary true if it is a primary item
658         */
659        public UnresolvedItem(String id, boolean local, boolean primary)
660        {
661            _itemId = id;
662            _local = local;
663            _primary = primary;
664        }
665        
666        /**
667         * Get the item id
668         * @return the item id
669         */
670        public String getId ()
671        {
672            return _itemId;
673        }
674        
675        /**
676         * Return true if it is a local item
677         * @return true if it is a local item
678         */
679        public boolean isLocalItem ()
680        {
681            return _local;
682        }
683        
684        /**
685         * Return true if it is a primary item
686         * @return true if it is a primary item
687         */
688        public boolean isPrimary ()
689        {
690            return _primary;
691        }
692    }
693}