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.cms.clientsideelement; 017 018import java.util.ArrayList; 019import java.util.Comparator; 020import java.util.HashSet; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026import java.util.TreeMap; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.configuration.DefaultConfiguration; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034 035import org.ametys.cms.content.RootContentHelper; 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.languages.LanguagesManager; 039import org.ametys.core.right.RightManager.RightResult; 040import org.ametys.core.ui.ClientSideElement; 041import org.ametys.core.ui.SimpleMenu; 042import org.ametys.core.ui.StaticClientSideElement; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.core.util.I18nUtils; 045import org.ametys.core.util.I18nizableTextKeyComparator; 046import org.ametys.runtime.i18n.I18nizableText; 047 048/** 049 * This element creates a menu with one gallery item per content type classified by category (default mode). 050 * 051 * This element supports an alternative display, to display content types into sub menu items (root menu items will be the categories), 052 * if 'show-in-menu' parameters is set to 'true'. This mode should be privileged if the number of content types is important (> 30) 053 * 054 * The user rights are checked. 055 */ 056public class ContentTypesGallery extends SimpleMenu 057{ 058 /** The list of content types */ 059 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 060 /** The language manager */ 061 protected LanguagesManager _languagesManager; 062 063 /** The i18n utils */ 064 protected I18nUtils _i18nUtils; 065 066 /** Helper for root content */ 067 protected RootContentHelper _rootContentHelper; 068 069 private boolean _contentTypesInitialized; 070 071 private Set<String> _addedComponents; 072 073 @Override 074 public void service(ServiceManager smanager) throws ServiceException 075 { 076 super.service(smanager); 077 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 078 _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE); 079 _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE); 080 _rootContentHelper = (RootContentHelper) smanager.lookup(RootContentHelper.ROLE); 081 } 082 083 @Override 084 protected Script _configureScript(Configuration configuration) throws ConfigurationException 085 { 086 Script script = super._configureScript(configuration); 087 088 // Add the CSS brought by content types 089 for (String id: _contentTypeExtensionPoint.getExtensionsIds()) 090 { 091 ContentType contentType = _contentTypeExtensionPoint.getExtension(id); 092 script.getCSSFiles().addAll(contentType.getCSSFiles()); 093 } 094 095 return script; 096 } 097 098 @Override 099 protected void _getGalleryItems(Map<String, Object> parameters, Map<String, Object> contextualParameters) 100 { 101 if (!_showInMenu()) 102 { 103 // In this mode (default mode), the available content types will be displayed into gallery items, classified by category 104 105 try 106 { 107 _lazyInitializeContentTypeGallery(); 108 } 109 catch (Exception e) 110 { 111 throw new IllegalStateException("Unable to lookup client side element local components", e); 112 } 113 114 if (_galleryItems.size() > 0) 115 { 116 parameters.put("gallery-item", new LinkedHashMap<String, Object>()); 117 118 for (GalleryItem galleryItem : _galleryItems) 119 { 120 @SuppressWarnings("unchecked") 121 Map<String, Object> galleryItems = (Map<String, Object>) parameters.get("gallery-item"); 122 galleryItems.put("gallery-groups", new ArrayList<>()); 123 124 for (GalleryGroup galleryGp : galleryItem.getGroups()) 125 { 126 @SuppressWarnings("unchecked") 127 List<Object> galleryGroups = (List<Object>) galleryItems.get("gallery-groups"); 128 129 Map<String, Object> groupParams = new LinkedHashMap<>(); 130 131 I18nizableText label = galleryGp.getLabel(); 132 groupParams.put("label", label); 133 134 // Group items sorted by alphabetical order 135 List<ClientSideElement> gpItems = new ArrayList<>(); 136 for (ClientSideElement element : galleryGp.getItems()) 137 { 138 String cTypeId = element.getId().substring(this.getId().length() + 1); 139 ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId); 140 141 if (hasRight(cType)) 142 { 143 gpItems.add(element); 144 } 145 } 146 147 if (gpItems.size() > 0) 148 { 149 List<String> gpItemIds = gpItems.stream().map(e -> e.getId()).collect(Collectors.toList()); 150 groupParams.put("items", gpItemIds); 151 galleryGroups.add(groupParams); 152 } 153 } 154 } 155 } 156 } 157 } 158 159 private boolean _showInMenu() 160 { 161 return _script.getParameters().containsKey("show-in-menu") && _script.getParameters().get("show-in-menu").equals("true"); 162 } 163 164 private synchronized void _lazyInitializeContentTypeGallery() throws ConfigurationException 165 { 166 if (!_contentTypesInitialized) 167 { 168 _addedComponents = new HashSet<>(); 169 170 Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup(); 171 172 if (cTypesByGroup.size() > 0) 173 { 174 GalleryItem galleryItem = new GalleryItem(); 175 176 for (I18nizableText groupLabel : cTypesByGroup.keySet()) 177 { 178 GalleryGroup galleryGroup = new GalleryGroup(groupLabel); 179 galleryItem.addGroup(galleryGroup); 180 181 Set<ContentType> cTypes = cTypesByGroup.get(groupLabel); 182 for (ContentType cType : cTypes) 183 { 184 String id = this.getId() + "-" + cType.getId(); 185 186 DefaultConfiguration conf = new DefaultConfiguration("extension"); 187 conf.setAttribute("id", id); 188 _addContentTypeConfiguration (conf, cType); 189 190 if (!_addedComponents.contains(id)) 191 { 192 _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf); 193 galleryGroup.addItem(new UnresolvedItem(id, true)); 194 _addedComponents.add(id); 195 } 196 } 197 } 198 199 _galleryItems.add(galleryItem); 200 } 201 202 if (_galleryItems.size() > 0) 203 { 204 try 205 { 206 _getGalleryItemManager().initialize(); 207 } 208 catch (Exception e) 209 { 210 throw new ConfigurationException("Unable to lookup parameter local components", e); 211 } 212 } 213 } 214 215 _contentTypesInitialized = true; 216 } 217 218 private synchronized void _lazyInitializeContentTypeMenu() 219 { 220 if (!_contentTypesInitialized) 221 { 222 _addedComponents = new HashSet<>(); 223 224 Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup(); 225 226 if (cTypesByGroup.size() > 0) 227 { 228 for (I18nizableText groupLabel : cTypesByGroup.keySet()) 229 { 230 String id = this.getId() + "-" + org.ametys.core.util.StringUtils.generateKey(); 231 DefaultConfiguration conf = new DefaultConfiguration("extension"); 232 conf.setAttribute("id", id); 233 234 Set<ContentType> cTypes = cTypesByGroup.get(groupLabel); 235 _addGroupContentTypeConfiguration (conf, groupLabel, cTypes); 236 237 if (!_addedComponents.contains(id)) 238 { 239 _menuItemManager.addComponent(_pluginName, null, id, SimpleMenu.class, conf); 240 _unresolvedMenuItems.add(new UnresolvedItem(id, true)); 241 _addedComponents.add(id); 242 } 243 } 244 } 245 } 246 247 _contentTypesInitialized = true; 248 } 249 250 /** 251 * Get the configuration of the content type item 252 * @param rootConf The root configuration 253 * @param groupLabel The group's label 254 * @param cTypes The content types of the group 255 */ 256 protected void _addGroupContentTypeConfiguration (DefaultConfiguration rootConf, I18nizableText groupLabel, Set<ContentType> cTypes) 257 { 258 DefaultConfiguration classConf = new DefaultConfiguration("class"); 259 classConf.setAttribute("name", "Ametys.plugins.cms.content.controller.ContentTypeMenuItemController"); 260 261 // Label and description 262 DefaultConfiguration labelConf = new DefaultConfiguration("label"); 263 DefaultConfiguration descConf = new DefaultConfiguration("description"); 264 if (groupLabel.isI18n()) 265 { 266 labelConf.setAttribute("i18n", "true"); 267 labelConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey()); 268 descConf.setAttribute("i18n", "true"); 269 descConf.setValue(groupLabel.getCatalogue() + ":" + groupLabel.getKey()); 270 } 271 else 272 { 273 labelConf.setValue(groupLabel.getLabel()); 274 descConf.setValue(groupLabel.getLabel()); 275 } 276 classConf.addChild(labelConf); 277 classConf.addChild(descConf); 278 279 // Icons or glyph (use the first content type of the list) 280 ContentType firstCtype = cTypes.iterator().next(); 281 if (firstCtype.getIconGlyph() != null) 282 { 283 DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph"); 284 iconGlyphConf.setValue(firstCtype.getIconGlyph()); 285 classConf.addChild(iconGlyphConf); 286 } 287 if (firstCtype.getIconDecorator() != null) 288 { 289 DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator"); 290 iconDecoratorConf.setValue(firstCtype.getIconDecorator()); 291 classConf.addChild(iconDecoratorConf); 292 } 293 if (firstCtype.getSmallIcon() != null) 294 { 295 DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small"); 296 iconSmallConf.setValue(firstCtype.getSmallIcon()); 297 classConf.addChild(iconSmallConf); 298 DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium"); 299 iconMediumConf.setValue(firstCtype.getMediumIcon()); 300 classConf.addChild(iconMediumConf); 301 DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large"); 302 iconLargeConf.setValue(firstCtype.getLargeIcon()); 303 classConf.addChild(iconLargeConf); 304 } 305 306 // Common configuration 307 @SuppressWarnings("unchecked") 308 Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("group-items-config"); 309 for (String tagName : commonConfig.keySet()) 310 { 311 DefaultConfiguration c = new DefaultConfiguration(tagName); 312 c.setValue(String.valueOf(commonConfig.get(tagName))); 313 classConf.addChild(c); 314 } 315 316 rootConf.addChild(classConf); 317 318 // Menu items 319 DefaultConfiguration menuItemsConf = new DefaultConfiguration("menu-items"); 320 321 for (ContentType contentType : cTypes) 322 { 323 DefaultConfiguration menuItemConf = new DefaultConfiguration("item"); 324 menuItemConf.setAttribute("id", this.getId() + "-" + contentType.getId()); 325 _addContentTypeConfiguration (menuItemConf, contentType); 326 327 menuItemsConf.addChild(menuItemConf); 328 } 329 330 rootConf.addChild(menuItemsConf); 331 } 332 333 /** 334 * Get the configuration of the content type item 335 * @param rootConf The root configuration 336 * @param cType The content type 337 */ 338 protected void _addContentTypeConfiguration (DefaultConfiguration rootConf, ContentType cType) 339 { 340 DefaultConfiguration classConf = new DefaultConfiguration("class"); 341 classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController"); 342 343 // enable toggle 344 DefaultConfiguration enableToggleConf = new DefaultConfiguration("toggle-enabled"); 345 enableToggleConf.setValue("true"); 346 classConf.addChild(enableToggleConf); 347 348 // Parent controller id 349 DefaultConfiguration parentIdConf = new DefaultConfiguration("controllerParentId"); 350 parentIdConf.setValue(this.getId()); 351 classConf.addChild(parentIdConf); 352 353 // Label 354 DefaultConfiguration labelConf = _getI18nizableTextConfiguration("label", cType.getLabel()); 355 classConf.addChild(labelConf); 356 357 // Description 358 DefaultConfiguration descConf = _getI18nizableTextConfiguration("description", cType.getDescription()); 359 classConf.addChild(descConf); 360 361 // Default content title 362 DefaultConfiguration defaultTitleConf = _getI18nizableTextConfiguration("defaultContentTitle", cType.getDefaultTitle()); 363 classConf.addChild(defaultTitleConf); 364 365 // Content type 366 DefaultConfiguration typeConf = new DefaultConfiguration("contentTypes"); 367 typeConf.setValue(cType.getId()); 368 classConf.addChild(typeConf); 369 370 // Icons or glyph 371 _addContentTypeIconsConfiguration(classConf, cType); 372 373 // Common configuration 374 @SuppressWarnings("unchecked") 375 Map<String, Object> commonConfig = (Map<String, Object>) this._script.getParameters().get("items-config"); 376 for (String tagName : commonConfig.keySet()) 377 { 378 DefaultConfiguration c = new DefaultConfiguration(tagName); 379 c.setValue(String.valueOf(commonConfig.get(tagName))); 380 classConf.addChild(c); 381 } 382 383 rootConf.addChild(classConf); 384 385 _addRightsOnContentTypeConfiguration(rootConf); 386 } 387 388 /** 389 * Get the configuration for an i18nizable text 390 * @param tagName the tag name 391 * @param i18nText the i18n text 392 * @return the configuration for i18nizable text 393 */ 394 protected DefaultConfiguration _getI18nizableTextConfiguration(String tagName, I18nizableText i18nText) 395 { 396 DefaultConfiguration i18nConf = new DefaultConfiguration(tagName); 397 if (i18nText.isI18n()) 398 { 399 i18nConf.setAttribute("i18n", "true"); 400 i18nConf.setValue(i18nText.getCatalogue() + ":" + i18nText.getKey()); 401 } 402 else 403 { 404 i18nConf.setValue(i18nText.getLabel()); 405 } 406 return i18nConf; 407 } 408 409 /** 410 * Get the 'rights' configuration of the content type item 411 * @param rootConf The root configuration 412 */ 413 protected void _addRightsOnContentTypeConfiguration(DefaultConfiguration rootConf) 414 { 415 DefaultConfiguration rightsConf = new DefaultConfiguration("rights"); 416 rightsConf.setAttribute("mode", _rightsMode); 417 for (String rightId : _rights.keySet()) 418 { 419 DefaultConfiguration rightConf = new DefaultConfiguration("right"); 420 Optional.ofNullable(_rights.get(rightId)).ifPresent(contextPrefix -> rightConf.setAttribute("context-prefix", contextPrefix)); 421 rightConf.setValue(rightId); 422 rightsConf.addChild(rightConf); 423 } 424 rootConf.addChild(rightsConf); 425 } 426 427 /** 428 * Add configuration for content type's icons and/or glyphes 429 * @param classConf the class configuration 430 * @param cType the content type 431 */ 432 protected void _addContentTypeIconsConfiguration(DefaultConfiguration classConf, ContentType cType) 433 { 434 if (cType.getIconGlyph() != null) 435 { 436 DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph"); 437 iconGlyphConf.setValue(cType.getIconGlyph()); 438 classConf.addChild(iconGlyphConf); 439 } 440 if (cType.getIconDecorator() != null) 441 { 442 DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator"); 443 iconDecoratorConf.setValue(cType.getIconDecorator()); 444 classConf.addChild(iconDecoratorConf); 445 } 446 if (cType.getSmallIcon() != null) 447 { 448 DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small"); 449 iconSmallConf.setValue(cType.getSmallIcon()); 450 classConf.addChild(iconSmallConf); 451 DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium"); 452 iconMediumConf.setValue(cType.getMediumIcon()); 453 classConf.addChild(iconMediumConf); 454 DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large"); 455 iconLargeConf.setValue(cType.getLargeIcon()); 456 classConf.addChild(iconLargeConf); 457 } 458 } 459 460 /** 461 * Get the list of content types classified by groups 462 * @return The content types 463 */ 464 protected Map<I18nizableText, Set<ContentType>> _getContentTypesByGroup () 465 { 466 Map<I18nizableText, Set<ContentType>> groups = new TreeMap<>(new I18nizableTextKeyComparator()); 467 468 if (this._script.getParameters().get("contentTypes") != null) 469 { 470 String[] contentTypesIds = ((String) this._script.getParameters().get("contentTypes")).split(","); 471 472 boolean allowInherit = "true".equals(this._script.getParameters().get("allowInherit")); 473 474 for (String contentTypeId : contentTypesIds) 475 { 476 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 477 478 if (isValidContentType(contentType)) 479 { 480 addContentType (contentType, groups); 481 } 482 483 if (allowInherit) 484 { 485 for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(contentTypeId)) 486 { 487 ContentType subContentType = _contentTypeExtensionPoint.getExtension(subTypeId); 488 if (isValidContentType(subContentType)) 489 { 490 addContentType (subContentType, groups); 491 } 492 } 493 } 494 } 495 } 496 else 497 { 498 Set<String> contentTypesIds = _contentTypeExtensionPoint.getExtensionsIds(); 499 500 for (String contentTypeId : contentTypesIds) 501 { 502 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 503 504 if (isValidContentType(contentType)) 505 { 506 addContentType (contentType, groups); 507 } 508 } 509 } 510 511 return groups; 512 } 513 514 /** 515 * Add content to groups 516 * @param contentType The content type 517 * @param groups The groups 518 */ 519 protected void addContentType (ContentType contentType, Map<I18nizableText, Set<ContentType>> groups) 520 { 521 I18nizableText group = contentType.getCategory(); 522 if ((group.isI18n() && group.getKey().isEmpty()) || (!group.isI18n() && group.getLabel().isEmpty())) 523 { 524 group = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT"); 525 } 526 527 if (!groups.containsKey(group)) 528 { 529 groups.put(group, new HashSet<>()); 530 } 531 Set<ContentType> cTypes = groups.get(group); 532 cTypes.add(contentType); 533 } 534 535 @Override 536 public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters) 537 { 538 if (_showInMenu()) 539 { 540 // In this mode, the available content types will be displayed into menu items, classified by category (sub menu items) 541 try 542 { 543 _lazyInitializeContentTypeMenu(); 544 } 545 catch (Exception e) 546 { 547 throw new IllegalStateException("Unable to lookup client side element local components", e); 548 } 549 } 550 551 for (String id: _contentTypeExtensionPoint.getExtensionsIds()) 552 { 553 ContentType contentType = _contentTypeExtensionPoint.getExtension(id); 554 if (isValidContentType(contentType)) 555 { 556 return super.getScripts(ignoreRights, contextParameters); 557 } 558 } 559 return new ArrayList<>(); 560 } 561 562 /** 563 * Determines if the content type is a valid content type for the gallery 564 * @param contentType The coentent 565 * @return true if it is a valid content type 566 */ 567 protected boolean isValidContentType (ContentType contentType) 568 { 569 return !contentType.isAbstract() && !contentType.isPrivate() && !contentType.isMixin() && !contentType.isReferenceTable(); 570 } 571 572 /** 573 * Test if the current user has the right needed by the content type to create a content. 574 * @param cType the content type 575 * @return true if the user has the right needed, false otherwise. 576 */ 577 protected boolean hasRight(ContentType cType) 578 { 579 String right = cType.getRight(); 580 581 if (right == null) 582 { 583 return true; 584 } 585 else 586 { 587 UserIdentity user = _currentUserProvider.getUser(); 588 return _rightManager.hasRight(user, right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.hasRight(user, right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW; 589 } 590 } 591 592 /** 593 * Comparator used to order the content types in the categories 594 * We use the translated labels to make it easier to find a content type 595 * But the order will be different for each language 596 */ 597 class ContentTypeClientSideElementComparator implements Comparator<ClientSideElement> 598 { 599 String _parentMenuId; 600 601 public ContentTypeClientSideElementComparator(String parentMenuId) 602 { 603 _parentMenuId = parentMenuId; 604 } 605 606 @Override 607 public int compare(ClientSideElement c1, ClientSideElement c2) 608 { 609 if (c1 == c2) 610 { 611 return 0; 612 } 613 614 String cTypeId1 = c1.getId().substring(_parentMenuId.length() + 1); 615 ContentType cType1 = _contentTypeExtensionPoint.getExtension(cTypeId1); 616 I18nizableText t1 = cType1.getLabel(); 617 618 String cTypeId2 = c2.getId().substring(_parentMenuId.length() + 1); 619 ContentType cType2 = _contentTypeExtensionPoint.getExtension(cTypeId2); 620 I18nizableText t2 = cType2.getLabel(); 621 622 String str1 = _i18nUtils.translate(t1); 623 if (str1 == null) 624 { 625 str1 = t1.isI18n() ? t1.getKey() : t1.getLabel(); 626 } 627 String str2 = _i18nUtils.translate(t2); 628 if (str2 == null) 629 { 630 str2 = t2.isI18n() ? t2.getKey() : t2.getLabel(); 631 } 632 633 int compareTo = str1.toLowerCase().compareTo(str2.toLowerCase()); 634 if (compareTo == 0) 635 { 636 // Content types could have same labels but there are not equals, so do not return 0 to add it in TreeSet 637 // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 638 return 1; 639 } 640 return compareTo; 641 } 642 } 643}