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<RibbonExclude>());
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     * @throws SAXException if an error occurs
810     */
811    public void saxRibbonDefinition(ContentHandler handler, Map<String, Object> contextualParameters) throws SAXException
812    {
813        _lazyInitialize();
814        Map<Tab, List<Group>> userTabGroups = _generateTabGroups(contextualParameters);
815        List<Element> currentAppMenu = _resolveReferences(contextualParameters, this._ribbonConfig.getAppMenu().getElements());
816        List<Element> currentUserMenu = _resolveReferences(contextualParameters, this._ribbonConfig.getUserMenu().getElements());
817        
818        handler.startPrefixMapping("i18n", "http://apache.org/cocoon/i18n/2.1");
819        XMLUtils.startElement(handler, "ribbon");
820        
821        XMLUtils.startElement(handler, "controls");
822        for (String controlId : this._controlsReferences)
823        {
824            ClientSideElement control = _ribbonControlManager.getExtension(controlId);
825            _saxClientSideElementHelper.saxDefinition("control", control, RibbonControlsManager.ROLE, handler, contextualParameters);
826            
827            if (control instanceof MenuClientSideElement)
828            {
829                _saxReferencedControl ((MenuClientSideElement) control, handler, contextualParameters);
830            }
831        }
832        XMLUtils.endElement(handler, "controls");
833
834        XMLUtils.startElement(handler, "tabsControls");
835        for (String tabId : this._tabsReferences)
836        {
837            ClientSideElement tab = _ribbonTabManager.getExtension(tabId);
838            _saxClientSideElementHelper.saxDefinition("tab", tab, RibbonTabsManager.ROLE, handler, contextualParameters);
839        }
840        XMLUtils.endElement(handler, "tabsControls");
841
842        XMLUtils.startElement(handler, "app-menu");
843        for (Element appMenu : currentAppMenu)
844        {
845            appMenu.toSAX(handler);
846        }
847        XMLUtils.endElement(handler, "app-menu");
848        
849        XMLUtils.startElement(handler, "user-menu");
850        for (Element userMenu : currentUserMenu)
851        {
852            userMenu.toSAX(handler);
853        }
854        XMLUtils.endElement(handler, "user-menu");
855
856        XMLUtils.startElement(handler, "tabs");
857        for (Entry<Tab, List<Group>> entry : userTabGroups.entrySet())
858        {
859            entry.getKey().saxGroups(handler, entry.getValue());
860        }
861        XMLUtils.endElement(handler, "tabs");
862
863        XMLUtils.endElement(handler, "ribbon");
864        handler.endPrefixMapping("i18n");
865    }
866    
867    /**
868     * Generate the tab groups for the current user and contextual parameters, based on the ribbon configured tab list _tabs.
869     * @param contextualParameters The contextual parameters
870     * @return The list of groups for the current user, mapped by tab
871     */
872    private Map<Tab, List<Group>> _generateTabGroups(Map<String, Object> contextualParameters)
873    {
874        Map<Tab, List<Group>> tabGroups = new LinkedHashMap<>();
875        for (Tab tab : _ribbonConfig.getTabs())
876        {
877            List<Group> tabGroup = new ArrayList<>();
878            
879            for (Group group : tab.getGroups())
880            {
881                Group newGroup = _createGroupForUser(group, contextualParameters);
882                if (!newGroup.isEmpty())
883                {
884                    tabGroup.add(newGroup);
885                }
886            }
887            
888            if (!tabGroup.isEmpty())
889            {
890                tabGroups.put(tab, tabGroup);
891            }
892        }
893        
894        _checkTabConsistency(tabGroups);
895        
896        return tabGroups;
897    }
898
899    private void _checkTabConsistency(Map<Tab, List<Group>> tabGroups)
900    {
901        for (Entry<Tab, List<Group>> entry : tabGroups.entrySet())
902        {
903            for (Group group : entry.getValue())
904            {
905                GroupSize large = group.getLargeGroupSize();
906                if (large != null)
907                {
908                    _checkTabConsistency(large);
909                }
910                GroupSize medium = group.getMediumGroupSize();
911                if (medium != null)
912                {
913                    _checkTabConsistency(medium);
914                }
915                GroupSize small = group.getSmallGroupSize();
916                if (small != null)
917                {
918                    _checkTabConsistency(small);
919                }
920            }
921        }
922    }
923
924    private void _checkTabConsistency(GroupSize group)
925    {
926        List<Layout> layoutsBuffer = new ArrayList<>();
927        List<Element> originalChildren = group.getChildren();
928        for (int i = 0; i < originalChildren.size(); i++)
929        {
930            Element originalChild = originalChildren.get(i);
931            if (originalChild instanceof Layout & originalChild.getColumns() == 1)
932            {
933                Layout originalLayout = (Layout) originalChild;
934                if (layoutsBuffer.size() > 0)
935                {
936                    CONTROLSIZE originalLayoutSize = originalLayout.getSize();
937                    LAYOUTALIGN originalLayoutAlign = originalLayout.getAlign();
938                    if ((originalLayoutSize != null ? !originalLayoutSize.equals(layoutsBuffer.get(0).getSize()) : layoutsBuffer.get(0).getSize() != null) 
939                        || (originalLayoutAlign != null ? !originalLayoutAlign.equals(layoutsBuffer.get(0).getAlign()) : layoutsBuffer.get(0).getAlign() != null))                            
940                    {
941                        _checkLayoutsConsistency(layoutsBuffer, group);
942                    }
943                }
944                
945                layoutsBuffer.add(originalLayout);
946            }
947            else if (layoutsBuffer.size() > 0)
948            {
949                _checkLayoutsConsistency(layoutsBuffer, group);
950            }
951        }
952        
953        if (layoutsBuffer.size() > 0)
954        {
955            _checkLayoutsConsistency(layoutsBuffer, group);
956        }
957    }
958
959    private void _checkLayoutsConsistency(List<Layout> layoutsBuffer, GroupSize group)
960    {
961        LAYOUTALIGN align = layoutsBuffer.get(0).getAlign();
962        CONTROLSIZE size = layoutsBuffer.get(0).getSize();
963        int elementsPerLayout = LAYOUTALIGN.MIDDLE.equals(align) ? 2 : 3;
964        
965        while (layoutsBuffer.size() > 0)
966        {
967            Layout layout = layoutsBuffer.remove(0);
968            
969            // check if the existing colspan values are correct. If incorrect, offset the controls with more colspan
970            List<Element> elements = layout.getChildren();
971            int currentSize = elements.stream().mapToInt(Element::getColumns).sum();
972            
973            Layout newLayout = _checkLayoutsOverflow(size, elementsPerLayout, layout, elements, currentSize);
974            if (newLayout != null)
975            {
976                layoutsBuffer.add(0, newLayout);
977                group.getChildren().add(group.getChildren().indexOf(layout) + 1, newLayout);
978            }
979            
980            _checkLayoutsMerge(layoutsBuffer, elementsPerLayout, layout, elements, currentSize, group);
981        }
982        
983    }
984
985    private Layout _checkLayoutsOverflow(CONTROLSIZE size, int elementsPerLayout, Layout layout, List<Element> elements, int currentSize)
986    {
987        int layoutCols = layout.getColumns();
988        if (currentSize > layoutCols * elementsPerLayout)
989        {
990            // There are too many elements in this layout, probably due to increasing the colspan of an element. Split this layout into multiple layouts
991            Layout newLayout = new Layout(layout, size);
992            int position = 0;
993            for (Element element : elements)
994            {
995                position += element.getColumns();
996                if (position > layoutCols * elementsPerLayout)
997                {
998                    newLayout.getChildren().add(element);
999                }
1000            }
1001            elements.removeAll(newLayout.getChildren());
1002            
1003            return newLayout;
1004        }
1005        
1006        return null;
1007    }
1008
1009    private void _checkLayoutsMerge(List<Layout> layoutsBuffer, int elementsPerLayout, Layout layout, List<Element> elements, int layoutSize, GroupSize group)
1010    {
1011        int layoutCols = layout.getColumns();
1012        int currentSize = layoutSize;
1013        boolean canFitMore = currentSize < layoutCols * elementsPerLayout;
1014        
1015        while (canFitMore && layoutsBuffer.size() > 0)
1016        {
1017            // There is room for more elements, merge with the next layout
1018            Layout nextLayout = layoutsBuffer.get(0);
1019            
1020            if (nextLayout.getColumns() > layoutCols)
1021            {
1022                // increase layout cols to fit next layout elements
1023                layout.setColumns(nextLayout.getColumns());
1024                layoutCols = nextLayout.getColumns();
1025            }
1026            
1027            List<Element> nextChildren = nextLayout.getChildren();
1028            while (canFitMore && nextChildren.size() > 0)
1029            {
1030                Element nextChild = nextChildren.get(0);
1031                
1032                int nextChildColumns = nextChild.getColumns();
1033                if (nextChildColumns > layoutCols)
1034                {
1035                    // next element does not fit layout, due to an error in the original ribbon file. Increase layout size to fix it
1036                    layout.setColumns(nextChildColumns);
1037                    layoutCols = nextChildColumns;
1038                }
1039                
1040                int columnsLeft = layoutCols - (currentSize % layoutCols);
1041                if (columnsLeft < nextChildColumns)
1042                {
1043                    // increase colspan of previous element to fill the current line, so the next child can start at a new line to have enough space
1044                    Element previousElement = elements.get(elements.size() - 1);
1045                    previousElement.setColumns(previousElement.getColumns() + columnsLeft);
1046                    currentSize += columnsLeft;
1047                }
1048                
1049                if (currentSize + nextChildColumns <= layoutCols * elementsPerLayout)
1050                {
1051                    nextChildren.remove(nextChild);
1052                    elements.add(nextChild);
1053                    currentSize += nextChildColumns;
1054                }
1055                else
1056                {
1057                    canFitMore = false;   
1058                }
1059            }
1060            
1061            if (nextChildren.size() == 0)
1062            {
1063                layoutsBuffer.remove(nextLayout);
1064                group.getChildren().remove(nextLayout);
1065            }
1066            
1067            if (currentSize == layoutCols * elementsPerLayout)
1068            {
1069                canFitMore = false;
1070            }
1071        }
1072    }
1073
1074    /**
1075     * Create a contextualised group for the current user, based on the ribbon configuration group
1076     * @param ribbonGroup The group form the initial ribbon configuration
1077     * @param contextualParameters The contextual parameters
1078     * @return The group for the current user
1079     */
1080    private Group _createGroupForUser(Group ribbonGroup, Map<String, Object> contextualParameters)
1081    {
1082        Group group = new Group(ribbonGroup);
1083        GroupSize largeSize = group.getLargeGroupSize();
1084        GroupSize mediumSize = group.getMediumGroupSize();
1085        GroupSize smallSize = group.getSmallGroupSize();
1086        
1087        if (ribbonGroup.getLargeGroupSize() != null)
1088        {
1089            List<Element> largeElements = _resolveReferences(contextualParameters, ribbonGroup.getLargeGroupSize().getChildren());
1090            largeSize.getChildren().addAll(largeElements);
1091        }
1092        
1093        if (ribbonGroup.getMediumGroupSize() == null)
1094        {
1095            _generateGroupSizes(largeSize.getChildren(), mediumSize.getChildren(), false, largeSize.getControlIds().size());
1096            _generateGroupSizes(largeSize.getChildren(), smallSize.getChildren(), true, largeSize.getControlIds().size());
1097        }
1098        else
1099        {
1100            List<Element> mediumElements = _resolveReferences(contextualParameters, ribbonGroup.getMediumGroupSize().getChildren());
1101            mediumSize.getChildren().addAll(mediumElements);
1102            
1103            // Don't generate a small group if there is no <small> in the ribbon configuration
1104            if (ribbonGroup.getSmallGroupSize() != null)
1105            {
1106                List<Element> largeElements = _resolveReferences(contextualParameters, ribbonGroup.getSmallGroupSize().getChildren());
1107                smallSize.getChildren().addAll(largeElements);
1108            }
1109        }
1110        
1111        if (mediumSize.isSame(largeSize))
1112        {
1113            largeSize.getChildren().clear();
1114        }
1115        if (mediumSize.isSame(smallSize))
1116        {
1117            smallSize.getChildren().clear();
1118        }
1119        
1120        return group;
1121    }
1122    
1123    /**
1124     * Resolve all the controls references into the real ids for the contextual parameters, and the current user rights.
1125     * @param contextualParameters The contextual parameters
1126     * @param elements The elements to resolve
1127     * @return The list of resolved elements
1128     */
1129    private List<Element> _resolveReferences(Map<String, Object> contextualParameters, List<Element> elements)
1130    {
1131        List<Element> resolvedElements = new ArrayList<>();
1132        
1133        for (Element element : elements)
1134        {
1135            if (element instanceof ControlRef)
1136            {
1137                ControlRef controlRef = (ControlRef) element;
1138                ClientSideElement extension = _ribbonControlManager.getExtension(controlRef.getId());
1139                for (Script script : extension.getScripts(contextualParameters))
1140                {
1141                    resolvedElements.add(new ControlRef(script.getId(), controlRef.getColumns(), _logger));
1142                }
1143            }
1144            
1145            if (element instanceof Layout)
1146            {
1147                List<Element> layoutElements = _resolveReferences(contextualParameters, element.getChildren());
1148                if (layoutElements.size() > 0)
1149                {
1150                    Layout layout = (Layout) element;
1151                    Layout resolvedLayout = new Layout(layout, layout.getSize());
1152                    resolvedLayout.getChildren().addAll(layoutElements);
1153                    resolvedElements.add(resolvedLayout);
1154                }
1155            }
1156            
1157            if (element instanceof Toolbar)
1158            {
1159                List<Element> toolbarElements = _resolveReferences(contextualParameters, element.getChildren());
1160                if (toolbarElements.size() > 0)
1161                {
1162                    Toolbar toolbar = (Toolbar) element;
1163                    Toolbar resolvedToolbar = new Toolbar(_logger, toolbar.getColumns());
1164                    resolvedToolbar.getChildren().addAll(toolbarElements);
1165                    resolvedElements.add(resolvedToolbar);
1166                }
1167            }
1168            
1169            if (element instanceof Separator)
1170            {
1171                resolvedElements.add(element);
1172            }
1173        }
1174        
1175        // Remove separators at the beginning and the end 
1176        while (resolvedElements.size() > 0 && resolvedElements.get(0) instanceof Separator)
1177        {
1178            resolvedElements.remove(0);
1179        }
1180        while (resolvedElements.size() > 0 && resolvedElements.get(resolvedElements.size() - 1) instanceof Separator)
1181        {
1182            resolvedElements.remove(resolvedElements.size() - 1);
1183        }
1184        
1185        return resolvedElements;
1186    }
1187    
1188
1189    private void _generateGroupSizes(List<Element> largeElements, List<Element> groupSizeElements, boolean generateSmallSize, int groupTotalSize)
1190    {
1191        List<ControlRef> controlsQueue = new ArrayList<>();
1192        for (Element largeElement : largeElements)
1193        {
1194            if (largeElement instanceof ControlRef)
1195            {
1196                controlsQueue.add((ControlRef) largeElement);
1197            }
1198            
1199            if (largeElement instanceof Toolbar)
1200            {
1201                _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1202                controlsQueue.clear();
1203                
1204                Toolbar toolbar = (Toolbar) largeElement;
1205                groupSizeElements.add(toolbar);
1206            }
1207            
1208            if (largeElement instanceof Layout)
1209            {
1210                _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1211                controlsQueue.clear();
1212                
1213                Layout layout = (Layout) largeElement;
1214                Layout verySmallLayout = new Layout(layout, CONTROLSIZE.VERYSMALL);
1215                verySmallLayout.getChildren().addAll(layout.getChildren());
1216                
1217                groupSizeElements.add(verySmallLayout);
1218            }
1219        }
1220        
1221        _processControlRefsQueue(controlsQueue, groupSizeElements, groupTotalSize, generateSmallSize);
1222    }
1223    
1224    private void _processControlRefsQueue(List<ControlRef> controlsQueue, List<Element> groupSizeElements, int groupTotalSize, boolean generateSmallSize)
1225    {
1226        int queueSize = controlsQueue.size();
1227        int index = 0;
1228
1229        while (index < queueSize)
1230        {
1231            // grab the next batch of controls, at least 1 and up to 3 controls
1232            List<ControlRef> controlsBuffer = new ArrayList<>();
1233            while (controlsBuffer.size() == 0
1234                   || controlsBuffer.size() < 3 && controlsBuffer.size() + index != queueSize % 3)
1235            {
1236                controlsBuffer.add(controlsQueue.get(index + controlsBuffer.size()));
1237            }
1238            
1239            if (index == 0)
1240            {
1241                if (groupTotalSize > 1 && groupTotalSize <= 3)
1242                {
1243                    Layout newLayout = new Layout(1, CONTROLSIZE.SMALL, LAYOUTALIGN.TOP, _logger);
1244                    newLayout.getChildren().addAll(controlsBuffer);
1245                    groupSizeElements.add(newLayout);
1246                }
1247                else
1248                {
1249                    groupSizeElements.addAll(controlsBuffer);
1250                }
1251            }
1252            else
1253            {
1254                CONTROLSIZE controlSize = generateSmallSize && index >= 0 ? CONTROLSIZE.VERYSMALL : CONTROLSIZE.SMALL;
1255                Layout newLayout = new Layout(1, controlSize, LAYOUTALIGN.TOP, _logger);
1256                newLayout.getChildren().addAll(controlsBuffer);
1257                groupSizeElements.add(newLayout);
1258            }
1259            
1260            index += controlsBuffer.size();
1261        }
1262    }
1263
1264    private void _saxReferencedControl(MenuClientSideElement menu, ContentHandler handler, Map<String, Object> contextualParameters) throws SAXException
1265    {
1266        List<ClientSideElement> referencedControl = menu.getReferencedClientSideElements(contextualParameters);
1267        
1268        for (ClientSideElement element : referencedControl)
1269        {
1270            if (!this._controlsReferences.contains(element.getId()))
1271            {
1272                _saxClientSideElementHelper.saxDefinition("control", element, RibbonControlsManager.ROLE, handler, contextualParameters);
1273            }
1274            
1275            if (element instanceof MenuClientSideElement)
1276            {
1277                _saxReferencedControl ((MenuClientSideElement) element, handler, contextualParameters);
1278            }
1279        }
1280    }
1281
1282}