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