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