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}