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