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