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