001/* 002 * Copyright 2025 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.web.sitemap; 017 018import java.lang.reflect.Array; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import javax.jcr.Node; 026import javax.jcr.Property; 027import javax.jcr.RepositoryException; 028import javax.jcr.Value; 029 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.cocoon.components.ContextHelper; 039import org.apache.cocoon.environment.Request; 040 041import org.ametys.core.right.RightManager; 042import org.ametys.core.util.SizeUtils.ExcludeFromSizeCalculation; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.UnknownAmetysObjectException; 045import org.ametys.plugins.repository.data.holder.ModelLessDataHolder; 046import org.ametys.plugins.repository.data.holder.group.Composite; 047import org.ametys.plugins.repository.data.holder.group.Repeater; 048import org.ametys.plugins.repository.jcr.JCRAmetysObject; 049import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 050import org.ametys.runtime.model.type.ElementType; 051import org.ametys.web.repository.page.MetadataAwareSitemapElement; 052import org.ametys.web.repository.page.Page; 053import org.ametys.web.repository.page.Page.PageType; 054import org.ametys.web.repository.page.SitemapElement; 055import org.ametys.web.repository.sitemap.Sitemap; 056 057/** 058 * Default implementation of {@link SitemapTreeIndicator} 059 */ 060public class DefaultSitemapIndicator extends AbstractStaticSitemapIndicator implements Serviceable, Contextualizable 061{ 062 /** The Ametys object resolver */ 063 @ExcludeFromSizeCalculation 064 protected AmetysObjectResolver _resolver; 065 /** The right manager */ 066 @ExcludeFromSizeCalculation 067 protected RightManager _rightManager; 068 069 /** The tag condition. */ 070 public enum Condition 071 { 072 /** Or condition */ 073 OR, 074 /** And condition. */ 075 AND 076 } 077 078 private boolean _live; 079 private boolean _restricted; 080 private boolean _hidden; 081 private List<String> _tags; 082 private PageType _pageType; 083 private Map<String, String> _metadata; 084 private Map<String, String> _properties; 085 private Condition _tagsCondition; 086 private Condition _metadataCondition; 087 private Condition _propertiesCondition; 088 @ExcludeFromSizeCalculation 089 private Context _context; 090 091 @Override 092 public void service(ServiceManager manager) throws ServiceException 093 { 094 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 095 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 096 } 097 098 public void contextualize(Context context) throws ContextException 099 { 100 _context = context; 101 } 102 103 /** 104 * Create a new instance of {@link DefaultSitemapIndicator} 105 * @param id the unique id 106 * @param configuration the configuration 107 * @param manager the service manager 108 * @param context the avalon context 109 * @param defaultI18nCatalog the default i18n catalogue 110 * @param iconPathPrefix the prefix path for icon 111 * @return the sitemap indicator 112 * @throws ServiceException if an error occured while loading components 113 * @throws ConfigurationException if configuraion failed 114 * @throws ContextException if fail to initialize context 115 */ 116 public static DefaultSitemapIndicator newInstance(String id, Configuration configuration, ServiceManager manager, Context context, String defaultI18nCatalog, String iconPathPrefix) throws ServiceException, ConfigurationException, ContextException 117 { 118 DefaultSitemapIndicator indicator = new DefaultSitemapIndicator(); 119 indicator.service(manager); 120 indicator.contextualize(context); 121 indicator.configure(id, configuration, defaultI18nCatalog, iconPathPrefix); 122 return indicator; 123 } 124 125 @Override 126 public void configure(String id, Configuration configuration, String defaultI18nCatalog, String iconPathPrefix) throws ConfigurationException 127 { 128 super.configure(id, configuration, defaultI18nCatalog, iconPathPrefix); 129 Configuration conditionConf = configuration.getChild("conditions"); 130 this._tags = _configureTags(conditionConf.getChild("tags")); 131 this._pageType = _configurePageType(conditionConf.getChild("type")); 132 this._metadata = _configureMetadata(conditionConf.getChild("metadata")); 133 this._properties = _configureProperties(conditionConf.getChild("properties")); 134 this._live = conditionConf.getChild("live", false) != null; 135 this._restricted = conditionConf.getChild("restricted", false) != null; 136 this._hidden = conditionConf.getChild("hidden", false) != null; 137 } 138 139 @Override 140 public boolean matches(SitemapElement sitemapElement) 141 { 142 if (sitemapElement instanceof Page page && !_matchesTags(page)) 143 { 144 return false; 145 } 146 147 if (!_matchesMetadata((MetadataAwareSitemapElement) sitemapElement)) 148 { 149 return false; 150 } 151 152 if (!_matchesProperties(sitemapElement)) 153 { 154 return false; 155 } 156 157 if (!_matchesLive(sitemapElement)) 158 { 159 return false; 160 } 161 162 PageType pageType = getPageType(); 163 if (pageType != null && (sitemapElement instanceof Sitemap || sitemapElement instanceof Page page && !page.getType().equals(pageType))) 164 { 165 return false; 166 } 167 168 if (isRestricted() && _rightManager.hasAnonymousReadAccess(sitemapElement)) 169 { 170 return false; 171 } 172 173 if (!_matchesHidden(sitemapElement)) 174 { 175 return false; 176 } 177 178 return true; 179 } 180 181 /** 182 * Get the metadata condition 183 * @return the metadata condition 184 */ 185 public Condition getMetadataCondition () 186 { 187 return _metadataCondition; 188 } 189 190 /** 191 * The tags condition 192 * @return The tags condition 193 */ 194 public Condition getTagsCondition () 195 { 196 return _tagsCondition; 197 } 198 199 /** 200 * Get the metadata condition 201 * @return the metadata condition 202 */ 203 public Condition getPropertiesCondition () 204 { 205 return _propertiesCondition; 206 } 207 208 /** 209 * The tags 210 * @return The tags 211 */ 212 public List<String> getTags () 213 { 214 return _tags; 215 } 216 217 /** 218 * The page type 219 * @return The page type 220 */ 221 public PageType getPageType () 222 { 223 return _pageType; 224 } 225 226 /** 227 * Determines the live version 228 * @return true if this icon is for live version 229 */ 230 public boolean isLive () 231 { 232 return _live; 233 } 234 235 /** 236 * Determines the limited access 237 * @return true if this icon is for page with limited access 238 */ 239 public boolean isRestricted () 240 { 241 return _restricted; 242 } 243 244 /** 245 * Determines the hidden status 246 * @return true if this icon is for page with hidden status 247 */ 248 public boolean isHidden () 249 { 250 return _hidden; 251 } 252 253 /** 254 * Get metadata 255 * @return the metadata 256 */ 257 public Map<String, String> getMetadata () 258 { 259 return this._metadata; 260 } 261 262 /** 263 * Get properties 264 * @return the properties 265 */ 266 public Map<String, String> getProperties () 267 { 268 return this._properties; 269 } 270 271 private List<String> _configureTags (Configuration tagsConf) throws ConfigurationException 272 { 273 List<String> tags = new ArrayList<>(); 274 275 this._tagsCondition = Condition.valueOf(tagsConf.getAttribute("type", "AND").toUpperCase()); 276 277 Configuration[] children = tagsConf.getChildren("tag"); 278 for (Configuration child : children) 279 { 280 tags.add(child.getValue()); 281 } 282 return tags; 283 } 284 285 private PageType _configurePageType (Configuration child) 286 { 287 String value = child.getValue(null); 288 return value != null ? PageType.valueOf(value.toUpperCase()) : null; 289 } 290 291 private Map<String, String> _configureMetadata (Configuration metadataConf) throws ConfigurationException 292 { 293 Map<String, String> metadata = new HashMap<>(); 294 295 this._metadataCondition = Condition.valueOf(metadataConf.getAttribute("type", "AND").toUpperCase()); 296 297 Configuration[] children = metadataConf.getChildren("metadata"); 298 for (Configuration child : children) 299 { 300 metadata.put(child.getAttribute("name"), child.getValue("")); 301 } 302 return metadata; 303 } 304 305 private Map<String, String> _configureProperties (Configuration propConf) throws ConfigurationException 306 { 307 Map<String, String> properties = new HashMap<>(); 308 309 this._propertiesCondition = Condition.valueOf(propConf.getAttribute("type", "AND").toUpperCase()); 310 311 Configuration[] children = propConf.getChildren("property"); 312 for (Configuration child : children) 313 { 314 properties.put(child.getAttribute("name"), child.getValue("")); 315 } 316 return properties; 317 } 318 319 private boolean _matchesTags (Page page) 320 { 321 List<String> tags = getTags(); 322 323 if (!tags.isEmpty()) 324 { 325 Condition condition = getTagsCondition(); 326 if (condition == Condition.AND) 327 { 328 return page.getTags().containsAll(tags); 329 } 330 else if (condition == Condition.OR) 331 { 332 for (String tagName : tags) 333 { 334 if (page.getTags().contains(tagName)) 335 { 336 return true; 337 } 338 } 339 return false; 340 } 341 } 342 return true; 343 } 344 345 private boolean _matchesMetadata (MetadataAwareSitemapElement page) 346 { 347 Map<String, String> metadata = getMetadata(); 348 if (!metadata.isEmpty()) 349 { 350 Condition condition = getMetadataCondition(); 351 if (condition == Condition.AND) 352 { 353 return _matchesAndData(metadata, page); 354 } 355 else if (condition == Condition.OR) 356 { 357 return _matchesOrData(metadata, page); 358 } 359 } 360 return true; 361 } 362 363 private boolean _matchesProperties (SitemapElement page) 364 { 365 Map<String, String> properties = getProperties(); 366 if (!properties.isEmpty()) 367 { 368 Condition condition = getMetadataCondition(); 369 if (condition == Condition.AND) 370 { 371 return _matchesAndProperties(properties, page); 372 } 373 else if (condition == Condition.OR) 374 { 375 return _matchesOrProperties(properties, page); 376 } 377 } 378 return true; 379 } 380 381 private boolean _matchesAndProperties (Map<String, String> properties, SitemapElement page) 382 { 383 if (!(page instanceof JCRAmetysObject)) 384 { 385 return properties.size() == 0; 386 } 387 388 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 389 Node node = jcrPage.getNode(); 390 391 try 392 { 393 for (String propertyName : properties.keySet()) 394 { 395 String valueToTest = properties.get(propertyName); 396 397 if (!node.hasProperty(propertyName)) 398 { 399 // property does not exits 400 return false; 401 } 402 403 Property property = node.getProperty(propertyName); 404 if (property.getDefinition().isMultiple()) 405 { 406 Value[] values = property.getValues(); 407 if (valueToTest.length() > 0 && values.length == 0) 408 { 409 // metadata exits but is empty 410 return false; 411 } 412 else if (valueToTest.length() > 0) 413 { 414 String[] results = new String[values.length]; 415 for (int i = 0; i < values.length; i++) 416 { 417 Value value = values[i]; 418 results[i] = value.getString(); 419 } 420 421 List<String> asList = Arrays.asList(results); 422 String[] valuesToTest = valueToTest.split(","); 423 for (String val : valuesToTest) 424 { 425 if (!asList.contains(val)) 426 { 427 // values do not contain all test values 428 return false; 429 } 430 } 431 } 432 } 433 else 434 { 435 String value = property.getValue().getString(); 436 if (valueToTest.length() > 0 && !valueToTest.equals(value)) 437 { 438 // value is not equals to test value 439 return false; 440 } 441 } 442 } 443 } 444 catch (RepositoryException e) 445 { 446 getLogger().error("An error occurred while testing properties", e); 447 return false; 448 } 449 return true; 450 } 451 452 private boolean _matchesOrProperties (Map<String, String> properties, SitemapElement page) 453 { 454 if (!(page instanceof JCRAmetysObject)) 455 { 456 return properties.size() == 0; 457 } 458 459 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 460 Node node = jcrPage.getNode(); 461 462 try 463 { 464 for (String propertyName : properties.keySet()) 465 { 466 String valueToTest = properties.get(propertyName); 467 468 if (node.hasProperty(propertyName)) 469 { 470 Property property = node.getProperty(propertyName); 471 if (property.getDefinition().isMultiple()) 472 { 473 Value[] values = property.getValues(); 474 if (valueToTest.length() == 0 && values.length > 0) 475 { 476 // multiple metadata exists and is not empty 477 return true; 478 } 479 else if (valueToTest.length() != 0) 480 { 481 String[] results = new String[values.length]; 482 for (int i = 0; i < values.length; i++) 483 { 484 Value value = values[i]; 485 results[i] = value.getString(); 486 } 487 488 List<String> asList = Arrays.asList(results); 489 String[] valuesToTest = valueToTest.split(","); 490 boolean findAll = true; 491 for (String val : valuesToTest) 492 { 493 if (!asList.contains(val)) 494 { 495 findAll = false; 496 } 497 } 498 if (findAll) 499 { 500 // values contain all test values 501 return true; 502 } 503 } 504 } 505 else 506 { 507 String value = property.getString(); 508 if (valueToTest.length() == 0 || valueToTest.equals(value)) 509 { 510 // value is equals to test value 511 return true; 512 } 513 } 514 } 515 } 516 } 517 catch (RepositoryException e) 518 { 519 getLogger().error("An error occurred while testing properties", e); 520 return false; 521 } 522 523 return false; 524 } 525 526 private boolean _matchesLive(SitemapElement sitemapElement) 527 { 528 if (isLive()) 529 { 530 if (sitemapElement instanceof Sitemap) 531 { 532 return true; 533 } 534 else if (sitemapElement instanceof Page page) 535 { 536 Request request = ContextHelper.getRequest(_context); 537 String currentWp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 538 try 539 { 540 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "live"); 541 _resolver.resolveById(page.getId()); 542 } 543 catch (UnknownAmetysObjectException e) 544 { 545 return false; 546 } 547 finally 548 { 549 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWp); 550 } 551 return true; 552 } 553 } 554 555 return true; 556 } 557 558 @SuppressWarnings("unchecked") 559 private boolean _matchesAndData (Map<String, String> data, ModelLessDataHolder dataHolder) 560 { 561 for (String dataPath : data.keySet()) 562 { 563 if (!dataHolder.hasValueOrEmpty(dataPath)) 564 { 565 // data does not exits 566 return false; 567 } 568 569 Object value = dataHolder.getValue(dataPath); 570 571 if (value instanceof Composite) 572 { 573 if (((Composite) value).getDataNames().isEmpty()) 574 { 575 // the composite data is empty 576 return false; 577 } 578 } 579 else if (value instanceof Repeater) 580 { 581 if (((Repeater) value).getSize() <= 0) 582 { 583 // the repeater is empty 584 return false; 585 } 586 } 587 else 588 { 589 String valueToTest = data.get(dataPath); 590 if (!valueToTest.isEmpty() && !dataHolder.hasValue(dataPath)) 591 { 592 // data exits but is empty 593 return false; 594 } 595 else if (!valueToTest.isEmpty()) 596 { 597 ElementType type = (ElementType) dataHolder.getType(dataPath); 598 if (dataHolder.isMultiple(dataPath)) 599 { 600 if (!_findAllValuesInMultipleData(valueToTest, value, type)) 601 { 602 // value does not contain all test values 603 return false; 604 } 605 } 606 else 607 { 608 if (!valueToTest.equals(type.toString(value))) 609 { 610 // value is not equals to test value 611 return false; 612 } 613 } 614 } 615 } 616 } 617 618 // All data have matched 619 return true; 620 } 621 622 private boolean _matchesOrData (Map<String, String> data, ModelLessDataHolder dataHolder) 623 { 624 for (String dataPath : data.keySet()) 625 { 626 if (_matchesOrData(dataPath, data, dataHolder)) 627 { 628 return true; 629 } 630 } 631 632 return false; 633 } 634 635 @SuppressWarnings("unchecked") 636 private boolean _matchesOrData (String dataPath, Map<String, String> data, ModelLessDataHolder dataHolder) 637 { 638 if (!dataHolder.hasValueOrEmpty(dataPath)) 639 { 640 // data does not exists 641 return false; 642 } 643 644 Object value = dataHolder.getValue(dataPath); 645 646 if (value instanceof Composite) 647 { 648 if (!((Composite) value).getDataNames().isEmpty()) 649 { 650 // composite data is not empty, the condition matched 651 return true; 652 } 653 } 654 else if (value instanceof Repeater) 655 { 656 if (((Repeater) value).getSize() > 0) 657 { 658 // repeater data is not empty, the condition matched 659 return true; 660 } 661 } 662 else 663 { 664 String valueToTest = data.get(dataPath); 665 if (valueToTest.isEmpty()) 666 { 667 // No data to test 668 return true; 669 } 670 else if (dataHolder.hasValue(dataPath)) 671 { 672 ElementType type = (ElementType) dataHolder.getType(dataPath); 673 if (dataHolder.isMultiple(dataPath)) 674 { 675 if (_findAllValuesInMultipleData(valueToTest, value, type)) 676 { 677 // values contain all test values 678 return true; 679 } 680 } 681 else if (valueToTest.equals(type.toString(value))) 682 { 683 // value is equals to test value 684 return true; 685 } 686 } 687 } 688 689 return false; 690 } 691 692 private boolean _matchesHidden(SitemapElement sitemapElement) 693 { 694 if (isHidden()) 695 { 696 return sitemapElement instanceof Page page && !page.isVisible(); 697 } 698 699 return true; 700 } 701 702 private boolean _findAllValuesInMultipleData(String valueToTest, Object valueFromDataHolder, ElementType type) 703 { 704 List<String> valuesFromDataHolderAsString = new ArrayList<>(); 705 for (int i = 0; i < Array.getLength(valueFromDataHolder); i++) 706 { 707 @SuppressWarnings("unchecked") 708 String valueAsString = type.toString(Array.get(valueFromDataHolder, i)); 709 valuesFromDataHolderAsString.add(valueAsString); 710 } 711 712 String[] valuesToTest = valueToTest.split(","); 713 for (String value : valuesToTest) 714 { 715 if (!valuesFromDataHolderAsString.contains(value)) 716 { 717 return false; 718 } 719 } 720 721 return true; 722 } 723}