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.core.ui;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.Set;
029import java.util.function.Function;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032import java.util.stream.Collectors;
033
034import org.apache.avalon.framework.configuration.Configuration;
035import org.apache.avalon.framework.configuration.ConfigurationException;
036import org.apache.avalon.framework.configuration.DefaultConfiguration;
037import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
038import org.apache.cocoon.xml.XMLUtils;
039import org.apache.excalibur.source.Source;
040import org.apache.excalibur.source.SourceNotFoundException;
041import org.apache.excalibur.source.SourceResolver;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044import org.xml.sax.ContentHandler;
045import org.xml.sax.SAXException;
046
047import org.ametys.core.ui.ClientSideElement.Script;
048import org.ametys.core.ui.ribbonconfiguration.ControlRef;
049import org.ametys.core.ui.ribbonconfiguration.Element;
050import org.ametys.core.ui.ribbonconfiguration.Group;
051import org.ametys.core.ui.ribbonconfiguration.GroupSize;
052import org.ametys.core.ui.ribbonconfiguration.Layout;
053import org.ametys.core.ui.ribbonconfiguration.RibbonConfiguration;
054import org.ametys.core.ui.ribbonconfiguration.RibbonExclude;
055import org.ametys.core.ui.ribbonconfiguration.RibbonExclude.EXCLUDETARGET;
056import org.ametys.core.ui.ribbonconfiguration.RibbonExclude.EXCLUDETYPE;
057import org.ametys.core.ui.ribbonconfiguration.RibbonMenu;
058import org.ametys.core.ui.ribbonconfiguration.Separator;
059import org.ametys.core.ui.ribbonconfiguration.Tab;
060import org.ametys.core.ui.ribbonconfiguration.Toolbar;
061
062/**
063 * This class handles the ribbon configuration needed for client side display.
064 */
065public class RibbonConfigurationManager
066{
067    private static Logger _logger = LoggerFactory.getLogger(RibbonConfigurationManager.class);
068    
069    private static Pattern _PLUGINNAMEPATTERN = Pattern.compile("^plugin:([^:]+)://.*$");
070
071    /** Size constants for controls*/
072    public enum CONTROLSIZE 
073    {
074        /** Control is large size */ 
075        LARGE("large"), 
076        /** Control is small size */ 
077        SMALL("small"),
078        /** Control is very small (icon without text) size */ 
079        VERYSMALL("very-small");
080        
081        private String _value;
082        
083        private CONTROLSIZE(String value)
084        {
085            this._value = value;
086        }  
087           
088        @Override
089        public String toString() 
090        {
091            return _value;
092        }   
093
094        /**
095         * Converts a string to a CONTROLSIZE
096         * @param size The size to convert
097         * @return The size corresponding to the string or null if unknown
098         */
099        public static CONTROLSIZE createsFromString(String size)
100        {
101            for (CONTROLSIZE v : CONTROLSIZE.values())
102            {
103                if (v.toString().equals(size))
104                {
105                    return v;
106                }
107            }
108            return null;
109        }
110    }
111
112    /** Size constants for controls*/
113    public enum LAYOUTALIGN 
114    {
115        /** The controls are top aligned in the layout. Can be use with 1, 2 or 3 controls */ 
116        TOP("top"),
117        /** The controls are middly aligned. Can be used with 2 controls only. */ 
118        MIDDLE("middle");
119        
120        private String _value;
121        
122        private LAYOUTALIGN(String value)
123        {
124            this._value = value;   
125        }   
126           
127        @Override  
128        public String toString() 
129        {
130            return _value;
131        }   
132        
133        /**
134         * Converts a string to a layout align
135         * @param align The align to convert
136         * @return The align corresponding to the string or null if unknown
137         */
138        public static LAYOUTALIGN createsFromString(String align)
139        {
140            for (LAYOUTALIGN v : LAYOUTALIGN.values())
141            {
142                if (v.toString().equals(align))
143                {
144                    return v;
145                }
146            }
147            return null;
148        }
149    }
150    
151
152    /** The ribbon control manager */
153    protected RibbonManager _ribbonManager;
154    /** The ribbon tab extension point */
155    protected RibbonTabsManager _ribbonTabManager;
156    /** The sax clientside element helper */
157    protected SAXClientSideElementHelper _saxClientSideElementHelper;
158    /** The excalibur source resolver */
159    protected SourceResolver _resolver;
160    
161    /** The ribbon configuration, as read from the configuration source */
162    protected RibbonConfiguration _ribbonConfig;
163
164    /** The controls referenced by the ribbon */
165    protected Set<String> _controlsReferences = new HashSet<>();
166    /** The tabs referenced by the ribbon */
167    protected Set<String> _tabsReferences = new HashSet<>();
168    /** The ribbon controls manager */
169    protected RibbonControlsManager _ribbonControlManager;
170    /** The ribbon import manager */
171    protected RibbonImportManager _ribbonImportManager;
172    
173    private boolean _initialized;
174    
175    /**
176     * Constructor
177     * @param ribbonControlManager The ribbon control manager
178     * @param ribbonManager The ribbon manager for this config
179     * @param ribbonTabManager the ribbon tab manager
180     * @param ribbonImportManager The ribbon import manager
181     * @param saxClientSideElementHelper the helper to SAX client side element
182     * @param resolver the excalibur source resolver
183     * @param dependenciesManager The dependencies manager
184     * @param ribbonManagerCache The ribbon manager cache helper
185     * @param config the ribbon configuration
186     * @param workspaceName The current workspace name
187     * @throws RuntimeException if an error occurred
188     */
189    public RibbonConfigurationManager (RibbonControlsManager ribbonControlManager, RibbonManager ribbonManager, RibbonTabsManager ribbonTabManager, RibbonImportManager ribbonImportManager, SAXClientSideElementHelper saxClientSideElementHelper, SourceResolver resolver, ClientSideElementDependenciesManager dependenciesManager, RibbonManagerCache ribbonManagerCache, Source config, String workspaceName)
190    {
191        _ribbonControlManager = ribbonControlManager;
192        _ribbonManager = ribbonManager;
193        _ribbonTabManager = ribbonTabManager;
194        _ribbonImportManager = ribbonImportManager;
195        _saxClientSideElementHelper = saxClientSideElementHelper;
196        _resolver = resolver;
197        
198        try
199        {
200            _configure(config, dependenciesManager, ribbonManagerCache, workspaceName);
201        }
202        catch (Exception e)
203        {
204            throw new RuntimeException("Unable to read the configuration file", e);
205        }
206        
207        for (Entry<String, List<String>> entry : _ribbonConfig.getDependencies().entrySet())
208        {
209            String extensionPoint = entry.getKey();
210            for (String extensionId : entry.getValue())
211            {
212                dependenciesManager.register(extensionPoint, extensionId);
213            }
214        }
215    }
216
217    private void _configure(Source config, ClientSideElementDependenciesManager dependenciesManager, RibbonManagerCache ribbonManagerCache, String workspaceName) throws Exception
218    {
219        synchronized (_ribbonManager)
220        {
221            _ribbonConfig = ribbonManagerCache.getCachedConfiguration(_ribbonManager);
222            if (_ribbonConfig != null)
223            {
224                // configuration already cached
225                return;
226            }
227            
228            _ribbonConfig = new RibbonConfiguration();
229            Configuration configuration;
230            if (config.exists())
231            {
232                configuration = new DefaultConfigurationBuilder().build(config.getInputStream());
233            }
234            else
235            {
236                configuration = new DefaultConfiguration("ribbon");
237            }
238            
239            Map<String, Long> importsValidity = new HashMap<>();
240            importsValidity.put(config.getURI(), config.getLastModified());
241            
242            Map<String, Map<String, Object>> imports = new HashMap<>();
243            Map<EXCLUDETYPE, List<RibbonExclude>> excluded = new HashMap<>();
244            for (EXCLUDETYPE excludetype : EXCLUDETYPE.values())
245            {
246                excluded.put(excludetype, new ArrayList<RibbonExclude>());
247            }
248            _configureExcluded(configuration, imports, excluded, workspaceName);
249            
250            _configureRibbon(configuration, dependenciesManager, imports, importsValidity, excluded, null);
251            
252            for (Entry<String, Map<String, Object>> entry : imports.entrySet())
253            {
254                if ("automatic".equals(entry.getValue().get("type")))
255                {
256                    _configureImport(dependenciesManager, importsValidity, entry.getKey(), imports, excluded);
257                }
258            }
259            
260            Map<String, Tab> tabsLabelMapping = _ribbonConfig.getTabs().stream().filter(tab -> !tab.isOverride()).collect(Collectors.toMap(Tab::getLabel, Function.identity(), (tab1, tab2) -> tab1));
261            _configureTabOverride(tabsLabelMapping);
262            
263            List<String> excludedControls = excluded.entrySet().stream().filter(entry -> EXCLUDETYPE.CONTROL.equals(entry.getKey())).flatMap(entry -> entry.getValue().stream()).filter(exclude -> EXCLUDETARGET.ID.equals(exclude.getTarget())).map(RibbonExclude::getValue).collect(Collectors.toList());
264            for (Tab tab : _ribbonConfig.getTabs())
265            {
266                _removeExcludedControls(tab, excludedControls);   
267            }
268            
269            _configureTabOrder(tabsLabelMapping);
270            
271            ribbonManagerCache.addCachedConfiguration(_ribbonManager, _ribbonConfig, importsValidity);
272            _ribbonManager.initializeExtensions();
273        }
274    }
275    
276    private void _configureExcluded(Configuration configuration, Map<String, Map<String, Object>> imports, Map<EXCLUDETYPE, List<RibbonExclude>> excluded, String workspaceName) throws ConfigurationException
277    {
278        _configureExcludeFromImports(configuration, imports, excluded);
279        
280        // Automatic imports
281        for (String extensionId : _ribbonImportManager.getExtensionsIds())
282        {
283            RibbonImport ribbonImportExtension = _ribbonImportManager.getExtension(extensionId);
284            if (ribbonImportExtension != null)
285            {
286                for (Entry<List<String>, Pattern> importFiles : ribbonImportExtension.getImports().entrySet())
287                {
288                    if (importFiles.getValue().matcher(workspaceName).matches())
289                    {
290                        for (String importUri : importFiles.getKey())
291                        {
292                            _configureExcludeFromImports(importUri, imports, excluded);
293                            Map<String, Object> properties = imports.get(importUri);
294                            if (properties != null)
295                            {
296                                properties.put("extension", extensionId);
297                                properties.put("type", "automatic");
298                            }
299                        }
300                    }
301                }
302            }
303        }
304    }
305    
306    private void _configureExcludeFromImports(String url, Map<String, Map<String, Object>> imports, Map<EXCLUDETYPE, List<RibbonExclude>> excluded) throws ConfigurationException
307    {
308        if (!imports.containsKey(url))
309        {
310            Source src = null;
311            try
312            {
313                src = _resolver.resolveURI(url);
314                if (!src.exists())
315                {
316                    throw new SourceNotFoundException(url + " does not exists");
317                }
318                else
319                {
320                    try (InputStream is = src.getInputStream())
321                    {
322                        if (_logger.isDebugEnabled())
323                        {
324                            _logger.debug("RibbonConfigurationManager : new file imported '" + url + "'");
325                        }
326                        
327                        Configuration importedConfiguration = new DefaultConfigurationBuilder().build(is);
328                        Map<String, Object> properties = new HashMap<>();
329                        properties.put("configuration", importedConfiguration);
330                        properties.put("lastModified", src.getLastModified());
331                        imports.put(url, properties);
332                        _configureExcludeFromImports(importedConfiguration, imports, excluded);
333                    }
334                }
335            }
336            catch (Exception e)
337            {
338                throw new ConfigurationException("Cannot import file " + url, e);
339            }
340            finally
341            {
342                _resolver.release(src);
343            }
344        }
345    }
346    
347    private void _configureExcludeFromImports(Configuration configuration, Map<String, Map<String, Object>> imports, Map<EXCLUDETYPE, List<RibbonExclude>> excluded) throws ConfigurationException
348    {
349        for (Configuration excludeConf : configuration.getChild("exclude").getChildren())
350        {
351            RibbonExclude ribbonExclude = new RibbonExclude(excludeConf, _logger);
352            excluded.get(ribbonExclude.getType()).add(ribbonExclude);
353        }
354        
355        for (Configuration importConfig : configuration.getChild("tabs").getChildren("import"))
356        {
357            String url = importConfig.getValue();
358            
359            _configureExcludeFromImports(url, imports, excluded);
360        }
361    }
362    
363    private void _configureRibbon(Configuration configuration, ClientSideElementDependenciesManager dependenciesManager, Map<String, Map<String, Object>> imports, Map<String, Long> importValidity, Map<EXCLUDETYPE, List<RibbonExclude>> excludedList, String url) throws ConfigurationException
364    {
365        if (_logger.isDebugEnabled())
366        {
367            _logger.debug("Starting reading ribbon configuration");
368        }
369        
370        for (Configuration appMenuConfig : configuration.getChildren("app-menu"))
371        {
372            _configureRibbonMenu(appMenuConfig, _ribbonConfig.getAppMenu(), excludedList.get(EXCLUDETYPE.APPMENU), url != null ? imports.get(url) : null, url);
373        }
374        for (Configuration userMenuConfig : configuration.getChildren("user-menu"))
375        {
376            _configureRibbonMenu(userMenuConfig, _ribbonConfig.getUserMenu(), excludedList.get(EXCLUDETYPE.USERMENU), url != null ? imports.get(url) : null, url);
377        }
378        
379        Configuration[] dependenciesConfigurations = configuration.getChild("depends").getChildren();
380        for (Configuration dependencyConfigurations : dependenciesConfigurations)
381        {
382            String extensionPoint = dependencyConfigurations.getName();
383            String extensionId = dependencyConfigurations.getValue();
384            
385            _ribbonConfig.addDependency(extensionPoint, extensionId);
386        }
387        
388        Configuration[] tabsConfigurations = configuration.getChild("tabs").getChildren();
389        Integer defaultOrder = url != null ? Integer.MAX_VALUE : 0;
390        for (Configuration tabConfiguration : tabsConfigurations)
391        {
392            if ("tab".equals(tabConfiguration.getName()))
393            {
394                Tab tab = new Tab(tabConfiguration, _ribbonManager, defaultOrder, _logger);
395                
396                if (url == null || !_isTabExcluded(tab, excludedList))
397                {
398                    _ribbonConfig.getTabs().add(tab);
399                }
400                
401                // Only the first tab of the file has a default order
402                defaultOrder = null;
403            }
404            else if ("import".equals(tabConfiguration.getName()))
405            {
406                String importUrl = tabConfiguration.getValue();
407                _configureImport(dependenciesManager, importValidity, importUrl, imports, excludedList);
408            }
409        }
410
411        if (_logger.isDebugEnabled())
412        {
413            _logger.debug("Ending reading ribbon configuration");
414        }
415    }
416
417    private void _configureRibbonMenu(Configuration configuration, RibbonMenu ribbonMenu, List<RibbonExclude> excludedList, Map<String, Object> properties, String url) throws ConfigurationException
418    {
419        if (url != null && _isFileExcluded(properties, excludedList, url))
420        {
421            return;
422        }
423        
424        List<String> excludedControls = excludedList.stream().filter(exclude -> EXCLUDETARGET.ID.equals(exclude.getTarget())).map(RibbonExclude::getValue).collect(Collectors.toList());
425        
426        List<Element> elements = new ArrayList<>();
427        for (Configuration childConfig : configuration.getChildren())
428        {
429            if ("control".equals(childConfig.getName()))
430            {
431                if (!excludedControls.contains(childConfig.getAttribute("id", null)))
432                {
433                    elements.add(new ControlRef(childConfig, _ribbonManager, _logger));
434                }
435            }
436            else if ("separator".equals(childConfig.getName()))
437            {
438                elements.add(new Separator());
439            }
440            else
441            {
442                _logger.warn("During configuration of the ribbon, the app-menu or user-menu use an unknow tag '" + configuration.getName() + "'");
443            }
444        }
445        
446        ribbonMenu.addElements(elements, configuration.getAttribute("order", "0.10"), _logger);
447    }
448
449    private void _configureImport(ClientSideElementDependenciesManager dependenciesManager, Map<String, Long> importValidity, String url, Map<String, Map<String, Object>> imports, Map<EXCLUDETYPE, List<RibbonExclude>> excludedList) throws ConfigurationException
450    {
451        if (!imports.containsKey(url))
452        {
453            // unknown import
454            return;
455        }
456        
457        if (importValidity.containsKey(url) || _isFileExcluded(imports.get(url), excludedList.get(EXCLUDETYPE.IMPORT), url))
458        {
459            return;
460        }
461        
462        Map<String, Object> properties = imports.get(url);
463        if (properties.containsKey("configuration"))
464        {
465            Configuration configuration = (Configuration) properties.get("configuration");
466            importValidity.put(url, (long) properties.get("lastModified"));
467            _configureRibbon(configuration, dependenciesManager, imports, importValidity, excludedList, url);
468        }
469    }
470
471    private boolean _isFileExcluded(Map<String, Object> properties, List<RibbonExclude> excludedList, String url)
472    {
473        Matcher matcher = _PLUGINNAMEPATTERN.matcher(url);
474        String pluginName = matcher.matches() ? matcher.group(1) : null;
475        
476        for (RibbonExclude ribbonExclude : excludedList)
477        {
478            if (EXCLUDETARGET.EXTENSION.equals(ribbonExclude.getTarget()) && properties.containsKey("extension") && ribbonExclude.getValue().equals(properties.get("extension")))
479            {
480                if (_logger.isDebugEnabled())
481                {
482                    _logger.debug("RibbonConfigurationManager : The import '" + url + "' was not resolved because its extension '" + properties.get("extension") + "' is excluded.");
483                }
484
485                return true;
486            }
487            else if (EXCLUDETARGET.PLUGIN.equals(ribbonExclude.getTarget()) && pluginName != null && ribbonExclude.getValue().equals(pluginName))
488            {
489                if (_logger.isDebugEnabled())
490                {
491                    _logger.debug("RibbonConfigurationManager : The import '" + url + "' was not resolved because its plugin '" + pluginName + "' is excluded.");
492                }
493
494                return true;
495            }
496            else if (EXCLUDETARGET.FILE.equals(ribbonExclude.getTarget()) && ribbonExclude.getValue().equals(url))
497            {
498                if (_logger.isDebugEnabled())
499                {
500                    _logger.debug("RibbonConfigurationManager : The import '" + url + "' was not resolved because the file url is excluded.");
501                }
502
503                return true;
504            }
505        }
506        
507        return false;
508    }
509
510    private boolean _isTabExcluded(Tab tab, Map<EXCLUDETYPE, List<RibbonExclude>> excludedList)
511    {
512        String tabLabel = tab.getLabel();
513        for (RibbonExclude ribbonExclude : excludedList.get(EXCLUDETYPE.TAB))
514        {
515            if (EXCLUDETARGET.LABEL.equals(ribbonExclude.getTarget()) && ribbonExclude.getValue().equals(tabLabel))
516            {
517                if (_logger.isDebugEnabled())
518                {
519                    _logger.debug("RibbonConfigurationManager : The tab '" + tabLabel + "' was not added because it is excluded.");
520                }
521                
522                return true;
523            }
524        }
525        
526        return tab.getGroups().size() == 0;
527    }
528
529    private void _removeExcludedControls(Tab tab, List<String> excludedList)
530    {
531        for (Group group : tab.getGroups())
532        {
533            GroupSize largeGroupSize = group.getLargeGroupSize();
534            if (largeGroupSize != null)
535            {
536                _removeExcludedControls(largeGroupSize.getChildren(), excludedList);
537            }
538            GroupSize mediumGroupSize = group.getMediumGroupSize();
539            if (mediumGroupSize != null)
540            {
541                _removeExcludedControls(mediumGroupSize.getChildren(), excludedList);
542            }
543            GroupSize smallGroupSize = group.getSmallGroupSize();
544            if (smallGroupSize != null)
545            {
546                _removeExcludedControls(smallGroupSize.getChildren(), excludedList);
547            }
548        }
549    }
550    
551    private void _removeExcludedControls(List<Element> elements,  List<String> excludedList)
552    {
553        List<Element> elementsToRemove = new ArrayList<>();
554        
555        for (Element element : elements)
556        {
557            if (element instanceof ControlRef)
558            {
559                ControlRef control = (ControlRef) element;
560                if (excludedList.contains(control.getId()))
561                {
562                    if (_logger.isDebugEnabled())
563                    {
564                        _logger.debug("RibbonConfigurationManager : The control '" + control.getId() + "' was not added because it is excluded.");
565                    }
566
567                    elementsToRemove.add(element);
568                }
569            }
570            else
571            {
572                _removeExcludedControls(element.getChildren(), excludedList);
573                if (element.getChildren().size() == 0)
574                {
575                    elementsToRemove.add(element);
576                }
577            }
578        }
579        
580        elements.removeAll(elementsToRemove);
581    }
582
583    private void _configureTabOverride(Map<String, Tab> labelMapping)
584    {
585        List<Tab> tabsOverride = _ribbonConfig.getTabs().stream().filter(Tab::isOverride).collect(Collectors.toList());
586        for (Tab tab : tabsOverride)
587        {
588            if (labelMapping.containsKey(tab.getLabel()))
589            {
590                labelMapping.get(tab.getLabel()).injectGroups(tab.getGroups());
591            }
592        }
593        
594        for (Tab tab : tabsOverride)
595        {
596            if (labelMapping.containsKey(tab.getLabel()))
597            {
598                labelMapping.get(tab.getLabel()).injectGroupsOverride(tab.getGroups());
599            }
600        }
601        
602        _ribbonConfig.getTabs().removeAll(tabsOverride);
603    }
604    
605    private void _configureTabOrder(Map<String, Tab> labelMapping)
606    {
607        LinkedList<Tab> tabs = _ribbonConfig.getTabs();
608        
609        // Sort by non-contextual first 
610        Collections.sort(tabs, (tab1, tab2) -> tab1.isContextual() != tab2.isContextual() ? (tab1.isContextual() ? 1 : -1) : 0);
611        
612        // Move tabs whose order reference another tab
613        List<Tab> tabsToMove = tabs.stream().filter(tab -> tab.getOrderAsString() != null).collect(Collectors.toList());
614        for (Tab tab : tabsToMove)
615        {
616            String order = tab.getOrderAsString();
617            Tab referencedTab = order != null ? labelMapping.get(order) : null;
618            if (order != null && referencedTab != null && referencedTab != tab && referencedTab.isContextual() == tab.isContextual())
619            {
620                tabs.remove(tab);
621                int index = tabs.indexOf(referencedTab);
622                tabs.add(tab.orderBefore() ? index : index + 1, tab);
623                tab.setOrder(null);
624            }
625            else
626            {
627                _logger.warn("Invalid tab attribute order with value '" + order + "' for tab '" + tab.getId() + "'. Default tab order will be used instead");
628            }
629        }
630        
631        // Set order value for all then sort
632        Object previousOrder = null;
633        for (Tab tab : tabs)
634        {
635            Integer tabOrder = tab.getOrderAsInteger();
636            if (tabOrder == null)
637            {
638                tab.setOrder(previousOrder);
639            }
640            else
641            {
642                previousOrder = tabOrder;
643            }
644        }
645        Collections.sort(tabs, (tab1, tab2) -> tab1.isContextual() == tab2.isContextual() ? tab1.getOrderAsInteger() - tab2.getOrderAsInteger() : 0);
646    }
647    
648    /**
649     * Check that the configuration was correct
650     * @throws IllegalStateException if an item does not exist
651     */
652    private synchronized void _lazyInitialize() 
653    {
654        if (_initialized)
655        {
656            return;
657        }
658        
659        // check that all referred items does exist
660        for (Tab tab : _ribbonConfig.getTabs())
661        {
662            // Check this is an existing factory
663            if (tab.getId() != null)
664            {
665                ClientSideElement tabElement = _ribbonTabManager.getExtension(tab.getId());
666                if (tabElement == null)
667                {
668                    String errorMessage = "A tab item referes an unexisting item factory with id '" + tab.getId() + "'";
669                    _logger.error(errorMessage);
670                    throw new IllegalStateException(errorMessage);
671                }
672                else
673                {
674                    this._tabsReferences.add(tab.getId());
675                }
676            }
677            
678            // initialize groups
679            for (Group group : tab.getGroups())
680            {
681                _lazyInitialize(group.getLargeGroupSize());
682                _lazyInitialize(group.getMediumGroupSize());
683                _lazyInitialize(group.getSmallGroupSize());
684            }
685        }
686
687        _lazyInitialize(this._ribbonConfig.getAppMenu().getElements());
688        _lazyInitialize(this._ribbonConfig.getUserMenu().getElements());
689        
690        _initialized = true;
691    }
692
693    private void _lazyInitialize(GroupSize groupSize)
694    {
695        if (groupSize != null)
696        {
697            _lazyInitialize(groupSize.getChildren());
698    
699            for (Element element : groupSize.getChildren())
700            {
701                if (element instanceof Layout)
702                {
703                    Layout layout = (Layout) element;
704    
705                    _lazyInitialize(layout.getChildren());
706    
707                    for (Element layoutElement : layout.getChildren())
708                    {
709                        if (element instanceof Toolbar)
710                        {
711                            Toolbar toolbar = (Toolbar) layoutElement;
712    
713                            _lazyInitialize(toolbar.getChildren());
714                        }
715                    }                        
716                }
717            }
718        }
719    }
720    
721    private void _lazyInitialize(List<Element> elements)
722    {
723        for (Element element : elements)
724        {
725            if (element instanceof ControlRef)
726            {
727                ControlRef control = (ControlRef) element;
728                
729                // Check its an existing factory
730                ClientSideElement ribbonControl = _ribbonControlManager.getExtension(control.getId());
731                if (ribbonControl == null)
732                {
733                    String errorMessage = "An item referes an unexisting item factory with id '" + control.getId() + "'";
734                    _logger.error(errorMessage);
735                    throw new IllegalStateException(errorMessage);
736                }
737                else
738                {
739                    this._controlsReferences.add(control.getId());
740                }
741            }
742            else if (element instanceof Toolbar)
743            {
744                Toolbar toolbar = (Toolbar) element;
745                
746                _lazyInitialize(toolbar.getChildren());
747            }
748        }
749    }
750    
751    /**
752     * Retrieve the list of controls referenced by the ribbon
753     * @return The list of controls
754     */
755    public List<ClientSideElement> getControls()
756    {
757        List<ClientSideElement> controlsList = new ArrayList<>();
758        for (String controlId : this._controlsReferences)
759        {
760            ClientSideElement control = _ribbonControlManager.getExtension(controlId);
761            controlsList.add(control);
762            
763            if (control instanceof MenuClientSideElement)
764            {
765                controlsList.addAll(_getMenuControls((MenuClientSideElement) control));
766            }
767        }
768        
769        return controlsList;
770    }
771    
772    /**
773     * Retrieve the list of tabs referenced by the ribbon
774     * @return The list of tabs
775     */
776    public List<ClientSideElement> getTabs()
777    {
778        List<ClientSideElement> tabsList = new ArrayList<>();
779        for (String tabId : this._tabsReferences)
780        {
781            ClientSideElement tab = _ribbonTabManager.getExtension(tabId);
782            tabsList.add(tab);
783        }
784        
785        return tabsList;
786    }
787    
788    private List<ClientSideElement> _getMenuControls(MenuClientSideElement menu)
789    {
790        List<ClientSideElement> controlsList = new ArrayList<>();
791        for (ClientSideElement element : menu.getReferencedClientSideElements())
792        {
793            controlsList.add(element);
794            
795            if (element instanceof MenuClientSideElement)
796            {
797                controlsList.addAll(_getMenuControls((MenuClientSideElement) element));
798            }
799        }
800        return controlsList;
801    }
802
803    /**
804     * Sax the the initial configuration of the ribbon.
805     * @param handler The content handler where to sax
806     * @param contextualParameters Contextuals parameters transmitted by the environment.
807     * @throws SAXException if an error occurs
808     */
809    public void saxRibbonDefinition(ContentHandler handler, Map<String, Object> contextualParameters) throws SAXException
810    {
811        _lazyInitialize();
812        Map<Tab, List<Group>> userTabGroups = _generateTabGroups(contextualParameters);
813        List<Element> currentAppMenu = _resolveReferences(contextualParameters, this._ribbonConfig.getAppMenu().getElements());
814        List<Element> currentUserMenu = _resolveReferences(contextualParameters, this._ribbonConfig.getUserMenu().getElements());
815        
816        handler.startPrefixMapping("i18n", "http://apache.org/cocoon/i18n/2.1");
817        XMLUtils.startElement(handler, "ribbon");
818        
819        XMLUtils.startElement(handler, "controls");
820        for (String controlId : this._controlsReferences)
821        {
822            ClientSideElement control = _ribbonControlManager.getExtension(controlId);
823            _saxClientSideElementHelper.saxDefinition("control", control, handler, contextualParameters);
824            
825            if (control instanceof MenuClientSideElement)
826            {
827                _saxReferencedControl ((MenuClientSideElement) control, handler, contextualParameters);
828            }
829        }
830        XMLUtils.endElement(handler, "controls");
831
832        XMLUtils.startElement(handler, "tabsControls");
833        for (String tabId : this._tabsReferences)
834        {
835            ClientSideElement tab = _ribbonTabManager.getExtension(tabId);
836            _saxClientSideElementHelper.saxDefinition("tab", tab, handler, contextualParameters);
837        }
838        XMLUtils.endElement(handler, "tabsControls");
839
840        XMLUtils.startElement(handler, "app-menu");
841        for (Element appMenu : currentAppMenu)
842        {
843            appMenu.toSAX(handler);
844        }
845        XMLUtils.endElement(handler, "app-menu");
846        
847        XMLUtils.startElement(handler, "user-menu");
848        for (Element userMenu : currentUserMenu)
849        {
850            userMenu.toSAX(handler);
851        }
852        XMLUtils.endElement(handler, "user-menu");
853
854        XMLUtils.startElement(handler, "tabs");
855        for (Entry<Tab, List<Group>> entry : userTabGroups.entrySet())
856        {
857            entry.getKey().saxGroups(handler, entry.getValue());
858        }
859        XMLUtils.endElement(handler, "tabs");
860
861        XMLUtils.endElement(handler, "ribbon");
862        handler.endPrefixMapping("i18n");
863    }
864    
865    /**
866     * Generate the tab groups for the current user and contextual parameters, based on the ribbon configured tab list _tabs.
867     * @param contextualParameters The contextual parameters
868     * @return The list of groups for the current user, mapped by tab
869     */
870    private Map<Tab, List<Group>> _generateTabGroups(Map<String, Object> contextualParameters)
871    {
872        Map<Tab, List<Group>> tabGroups = new LinkedHashMap<>();
873        for (Tab tab : _ribbonConfig.getTabs())
874        {
875            List<Group> tabGroup = new ArrayList<>();
876            
877            for (Group group : tab.getGroups())
878            {
879                Group newGroup = _createGroupForUser(group, contextualParameters);
880                if (!newGroup.isEmpty())
881                {
882                    tabGroup.add(newGroup);
883                }
884            }
885            
886            if (!tabGroup.isEmpty())
887            {
888                tabGroups.put(tab, tabGroup);
889            }
890        }
891        
892        _checkTabConsistency(tabGroups);
893        
894        return tabGroups;
895    }
896
897    private void _checkTabConsistency(Map<Tab, List<Group>> tabGroups)
898    {
899        for (Entry<Tab, List<Group>> entry : tabGroups.entrySet())
900        {
901            for (Group group : entry.getValue())
902            {
903                GroupSize large = group.getLargeGroupSize();
904                if (large != null)
905                {
906                    _checkTabConsistency(large);
907                }
908                GroupSize medium = group.getMediumGroupSize();
909                if (medium != null)
910                {
911                    _checkTabConsistency(medium);
912                }
913                GroupSize small = group.getSmallGroupSize();
914                if (small != null)
915                {
916                    _checkTabConsistency(small);
917                }
918            }
919        }
920    }
921
922    private void _checkTabConsistency(GroupSize group)
923    {
924        List<Layout> layoutsBuffer = new ArrayList<>();
925        List<Element> originalChildren = group.getChildren();
926        for (int i = 0; i < originalChildren.size(); i++)
927        {
928            Element originalChild = originalChildren.get(i);
929            if (originalChild instanceof Layout & originalChild.getColumns() == 1)
930            {
931                Layout originalLayout = (Layout) originalChild;
932                if (layoutsBuffer.size() > 0)
933                {
934                    CONTROLSIZE originalLayoutSize = originalLayout.getSize();
935                    LAYOUTALIGN originalLayoutAlign = originalLayout.getAlign();
936                    if ((originalLayoutSize != null ? !originalLayoutSize.equals(layoutsBuffer.get(0).getSize()) : layoutsBuffer.get(0).getSize() != null) 
937                        || (originalLayoutAlign != null ? !originalLayoutAlign.equals(layoutsBuffer.get(0).getAlign()) : layoutsBuffer.get(0).getAlign() != null))                            
938                    {
939                        _checkLayoutsConsistency(layoutsBuffer, group);
940                    }
941                }
942                
943                layoutsBuffer.add(originalLayout);
944            }
945            else if (layoutsBuffer.size() > 0)
946            {
947                _checkLayoutsConsistency(layoutsBuffer, group);
948            }
949        }
950        
951        if (layoutsBuffer.size() > 0)
952        {
953            _checkLayoutsConsistency(layoutsBuffer, group);
954        }
955    }
956
957    private void _checkLayoutsConsistency(List<Layout> layoutsBuffer, GroupSize group)
958    {
959        LAYOUTALIGN align = layoutsBuffer.get(0).getAlign();
960        CONTROLSIZE size = layoutsBuffer.get(0).getSize();
961        int elementsPerLayout = LAYOUTALIGN.MIDDLE.equals(align) ? 2 : 3;
962        
963        while (layoutsBuffer.size() > 0)
964        {
965            Layout layout = layoutsBuffer.remove(0);
966            
967            // check if the existing colspan values are correct. If incorrect, offset the controls with more colspan
968            List<Element> elements = layout.getChildren();
969            int currentSize = elements.stream().mapToInt(Element::getColumns).sum();
970            
971            Layout newLayout = _checkLayoutsOverflow(size, elementsPerLayout, layout, elements, currentSize);
972            if (newLayout != null)
973            {
974                layoutsBuffer.add(0, newLayout);
975                group.getChildren().add(group.getChildren().indexOf(layout) + 1, newLayout);
976            }
977            
978            _checkLayoutsMerge(layoutsBuffer, elementsPerLayout, layout, elements, currentSize, group);
979        }
980        
981    }
982
983    private Layout _checkLayoutsOverflow(CONTROLSIZE size, int elementsPerLayout, Layout layout, List<Element> elements, int currentSize)
984    {
985        int layoutCols = layout.getColumns();
986        if (currentSize > layoutCols * elementsPerLayout)
987        {
988            // There are too many elements in this layout, probably due to increasing the colspan of an element. Split this layout into multiple layouts
989            Layout newLayout = new Layout(layout, size);
990            int position = 0;
991            for (Element element : elements)
992            {
993                position += element.getColumns();
994                if (position > layoutCols * elementsPerLayout)
995                {
996                    newLayout.getChildren().add(element);
997                }
998            }
999            elements.removeAll(newLayout.getChildren());
1000            
1001            return newLayout;
1002        }
1003        
1004        return null;
1005    }
1006
1007    private void _checkLayoutsMerge(List<Layout> layoutsBuffer, int elementsPerLayout, Layout layout, List<Element> elements, int layoutSize, GroupSize group)
1008    {
1009        int layoutCols = layout.getColumns();
1010        int currentSize = layoutSize;
1011        boolean canFitMore = currentSize < layoutCols * elementsPerLayout;
1012        
1013        while (canFitMore && layoutsBuffer.size() > 0)
1014        {
1015            // There is room for more elements, merge with the next layout
1016            Layout nextLayout = layoutsBuffer.get(0);
1017            
1018            if (nextLayout.getColumns() > layoutCols)
1019            {
1020                // increase layout cols to fit next layout elements
1021                layout.setColumns(nextLayout.getColumns());
1022                layoutCols = nextLayout.getColumns();
1023            }
1024            
1025            List<Element> nextChildren = nextLayout.getChildren();
1026            while (canFitMore && nextChildren.size() > 0)
1027            {
1028                Element nextChild = nextChildren.get(0);
1029                
1030                int nextChildColumns = nextChild.getColumns();
1031                if (nextChildColumns > layoutCols)
1032                {
1033                    // next element does not fit layout, due to an error in the original ribbon file. Increase layout size to fix it
1034                    layout.setColumns(nextChildColumns);
1035                    layoutCols = nextChildColumns;
1036                }
1037                
1038                int columnsLeft = layoutCols - (currentSize % layoutCols);
1039                if (columnsLeft < nextChildColumns)
1040                {
1041                    // increase colspan of previous element to fill the current line, so the next child can start at a new line to have enough space
1042                    Element previousElement = elements.get(elements.size() - 1);
1043                    previousElement.setColumns(previousElement.getColumns() + columnsLeft);
1044                    currentSize += columnsLeft;
1045                }
1046                
1047                if (currentSize + nextChildColumns <= layoutCols * elementsPerLayout)
1048                {
1049                    nextChildren.remove(nextChild);
1050                    elements.add(nextChild);
1051                    currentSize += nextChildColumns;
1052                }
1053                else
1054                {
1055                    canFitMore = false;   
1056                }
1057            }
1058            
1059            if (nextChildren.size() == 0)
1060            {
1061                layoutsBuffer.remove(nextLayout);
1062                group.getChildren().remove(nextLayout);
1063            }
1064            
1065            if (currentSize == layoutCols * elementsPerLayout)
1066            {
1067                canFitMore = false;
1068            }
1069        }
1070    }
1071
1072    /**
1073     * Create a contextualised group for the current user, based on the ribbon configuration group
1074     * @param ribbonGroup The group form the initial ribbon configuration
1075     * @param contextualParameters The contextual parameters
1076     * @return The group for the current user
1077     */
1078    private Group _createGroupForUser(Group ribbonGroup, Map<String, Object> contextualParameters)
1079    {
1080        Group group = new Group(ribbonGroup);
1081        GroupSize largeSize = group.getLargeGroupSize();
1082        GroupSize mediumSize = group.getMediumGroupSize();
1083        GroupSize smallSize = group.getSmallGroupSize();
1084        
1085        if (ribbonGroup.getLargeGroupSize() != null)
1086        {
1087            List<Element> largeElements = _resolveReferences(contextualParameters, ribbonGroup.getLargeGroupSize().getChildren());
1088            largeSize.getChildren().addAll(largeElements);
1089        }
1090        
1091        if (ribbonGroup.getMediumGroupSize() == null)
1092        {
1093            _generateGroupSizes(largeSize.getChildren(), mediumSize.getChildren(), false, largeSize.getControlIds().size());
1094            _generateGroupSizes(largeSize.getChildren(), smallSize.getChildren(), true, largeSize.getControlIds().size());
1095        }
1096        else
1097        {
1098            List<Element> mediumElements = _resolveReferences(contextualParameters, ribbonGroup.getMediumGroupSize().getChildren());
1099            mediumSize.getChildren().addAll(mediumElements);
1100            
1101            // Don't generate a small group if there is no <small> in the ribbon configuration
1102            if (ribbonGroup.getSmallGroupSize() != null)
1103            {
1104                List<Element> largeElements = _resolveReferences(contextualParameters, ribbonGroup.getSmallGroupSize().getChildren());
1105                smallSize.getChildren().addAll(largeElements);
1106            }
1107        }
1108        
1109        if (mediumSize.isSame(largeSize))
1110        {
1111            largeSize.getChildren().clear();
1112        }
1113        if (mediumSize.isSame(smallSize))
1114        {
1115            smallSize.getChildren().clear();
1116        }
1117        
1118        return group;
1119    }
1120    
1121    /**
1122     * Resolve all the controls references into the real ids for the contextual parameters, and the current user rights.
1123     * @param contextualParameters The contextual parameters
1124     * @param elements The elements to resolve
1125     * @return The list of resolved elements
1126     */
1127    private List<Element> _resolveReferences(Map<String, Object> contextualParameters, List<Element> elements)
1128    {
1129        List<Element> resolvedElements = new ArrayList<>();
1130        
1131        for (Element element : elements)
1132        {
1133            if (element instanceof ControlRef)
1134            {
1135                ControlRef controlRef = (ControlRef) element;
1136                ClientSideElement extension = _ribbonControlManager.getExtension(controlRef.getId());
1137                for (Script script : extension.getScripts(contextualParameters))
1138                {
1139                    resolvedElements.add(new ControlRef(script.getId(), controlRef.getColumns(), _logger));
1140                }
1141            }
1142            
1143            if (element instanceof Layout)
1144            {
1145                List<Element> layoutElements = _resolveReferences(contextualParameters, element.getChildren());
1146                if (layoutElements.size() > 0)
1147                {
1148                    Layout layout = (Layout) element;
1149                    Layout resolvedLayout = new Layout(layout, layout.getSize());
1150                    resolvedLayout.getChildren().addAll(layoutElements);
1151                    resolvedElements.add(resolvedLayout);
1152                }
1153            }
1154            
1155            if (element instanceof Toolbar)
1156            {
1157                List<Element> toolbarElements = _resolveReferences(contextualParameters, element.getChildren());
1158                if (toolbarElements.size() > 0)
1159                {
1160                    Toolbar toolbar = (Toolbar) element;
1161                    Toolbar resolvedToolbar = new Toolbar(_logger, toolbar.getColumns());
1162                    resolvedToolbar.getChildren().addAll(toolbarElements);
1163                    resolvedElements.add(resolvedToolbar);
1164                }
1165            }
1166            
1167            if (element instanceof Separator)
1168            {
1169                resolvedElements.add(element);
1170            }
1171        }
1172        
1173        // Remove separators at the beginning and the end 
1174        while (resolvedElements.size() > 0 && resolvedElements.get(0) instanceof Separator)
1175        {
1176            resolvedElements.remove(0);
1177        }
1178        while (resolvedElements.size() > 0 && resolvedElements.get(resolvedElements.size() - 1) instanceof Separator)
1179        {
1180            resolvedElements.remove(resolvedElements.size() - 1);
1181        }
1182        
1183        return resolvedElements;
1184    }
1185    
1186
1187    private void _generateGroupSizes(List<Element> largeElements, List<Element> groupSizeElements, boolean generateSmallSize, int groupTotalSize)
1188    {
1189        List<ControlRef> controlsQueue = new ArrayList<>();
1190        for (Element largeElement : largeElements)
1191        {
1192            if (largeElement instanceof ControlRef)
1193            {
1194                controlsQueue.add((ControlRef) largeElement);
1195            }
1196            
1197            if (largeElement instanceof Toolbar)
1198            {
1199                _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1200                controlsQueue.clear();
1201                
1202                Toolbar toolbar = (Toolbar) largeElement;
1203                groupSizeElements.add(toolbar);
1204            }
1205            
1206            if (largeElement instanceof Layout)
1207            {
1208                _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1209                controlsQueue.clear();
1210                
1211                Layout layout = (Layout) largeElement;
1212                Layout verySmallLayout = new Layout(layout, CONTROLSIZE.VERYSMALL);
1213                verySmallLayout.getChildren().addAll(layout.getChildren());
1214                
1215                groupSizeElements.add(verySmallLayout);
1216            }
1217        }
1218        
1219        _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1220    }
1221    
1222    private void _processControlRefsQueue(List<ControlRef> controlsQueue, List<Element> groupSizeElements, int groupTotalSize, boolean generateSmallSize)
1223    {
1224        int queueSize = controlsQueue.size();
1225        int index = 0;
1226
1227        while (index < queueSize)
1228        {
1229            // grab the next batch of controls, at least 1 and up to 3 controls
1230            List<ControlRef> controlsBuffer = new ArrayList<>();
1231            while (controlsBuffer.size() == 0 || (controlsBuffer.size() < 3 && controlsBuffer.size() + index != queueSize % 3))
1232            {
1233                controlsBuffer.add(controlsQueue.get(index + controlsBuffer.size()));
1234            }
1235            
1236            if (index == 0)
1237            {
1238                if (groupTotalSize > 1 && groupTotalSize <= 3)
1239                {
1240                    Layout newLayout = new Layout(1, CONTROLSIZE.SMALL, LAYOUTALIGN.TOP, _logger);
1241                    newLayout.getChildren().addAll(controlsBuffer);
1242                    groupSizeElements.add(newLayout);
1243                }
1244                else
1245                {
1246                    groupSizeElements.addAll(controlsBuffer);
1247                }
1248            }
1249            else
1250            {
1251                CONTROLSIZE controlSize = generateSmallSize && index >= 0 ? CONTROLSIZE.VERYSMALL : CONTROLSIZE.SMALL;
1252                Layout newLayout = new Layout(1, controlSize, LAYOUTALIGN.TOP, _logger);
1253                newLayout.getChildren().addAll(controlsBuffer);
1254                groupSizeElements.add(newLayout);
1255            }
1256            
1257            index += controlsBuffer.size();
1258        }
1259    }
1260
1261    private void _saxReferencedControl(MenuClientSideElement menu, ContentHandler handler, Map<String, Object> contextualParameters) throws SAXException
1262    {
1263        List<ClientSideElement> referencedControl = menu.getReferencedClientSideElements();
1264        
1265        for (ClientSideElement element : referencedControl)
1266        {
1267            if (!this._controlsReferences.contains(element.getId()))
1268            {
1269                _saxClientSideElementHelper.saxDefinition("control", element, handler, contextualParameters);
1270            }
1271            
1272            if (element instanceof MenuClientSideElement)
1273            {
1274                _saxReferencedControl ((MenuClientSideElement) element, handler, contextualParameters);
1275            }
1276        }
1277    }
1278}