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