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 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 } 136 137 @Override 138 public boolean matches(SitemapElement sitemapElement) 139 { 140 if (sitemapElement instanceof Page page && !_matchesTags(page)) 141 { 142 return false; 143 } 144 145 if (!_matchesMetadata((MetadataAwareSitemapElement) sitemapElement)) 146 { 147 return false; 148 } 149 150 if (!_matchesProperties(sitemapElement)) 151 { 152 return false; 153 } 154 155 if (isLive() && (sitemapElement instanceof Sitemap 156 || sitemapElement instanceof Page page && !_matchesLivePage(page))) 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 return true; 173 } 174 175 /** 176 * Get the metadata condition 177 * @return the metadata condition 178 */ 179 public Condition getMetadataCondition () 180 { 181 return _metadataCondition; 182 } 183 184 /** 185 * The tags condition 186 * @return The tags condition 187 */ 188 public Condition getTagsCondition () 189 { 190 return _tagsCondition; 191 } 192 193 /** 194 * Get the metadata condition 195 * @return the metadata condition 196 */ 197 public Condition getPropertiesCondition () 198 { 199 return _propertiesCondition; 200 } 201 202 /** 203 * The tags 204 * @return The tags 205 */ 206 public List<String> getTags () 207 { 208 return _tags; 209 } 210 211 /** 212 * The page type 213 * @return The page type 214 */ 215 public PageType getPageType () 216 { 217 return _pageType; 218 } 219 220 /** 221 * Determines the live version 222 * @return true if this icon is for live version 223 */ 224 public boolean isLive () 225 { 226 return _live; 227 } 228 229 /** 230 * Determines the limited access 231 * @return true if this icon is for page with limited access 232 */ 233 public boolean isRestricted () 234 { 235 return _restricted; 236 } 237 238 /** 239 * Get metadata 240 * @return the metadata 241 */ 242 public Map<String, String> getMetadata () 243 { 244 return this._metadata; 245 } 246 247 /** 248 * Get properties 249 * @return the properties 250 */ 251 public Map<String, String> getProperties () 252 { 253 return this._properties; 254 } 255 256 private List<String> _configureTags (Configuration tagsConf) throws ConfigurationException 257 { 258 List<String> tags = new ArrayList<>(); 259 260 this._tagsCondition = Condition.valueOf(tagsConf.getAttribute("type", "AND").toUpperCase()); 261 262 Configuration[] children = tagsConf.getChildren("tag"); 263 for (Configuration child : children) 264 { 265 tags.add(child.getValue()); 266 } 267 return tags; 268 } 269 270 private PageType _configurePageType (Configuration child) 271 { 272 String value = child.getValue(null); 273 return value != null ? PageType.valueOf(value.toUpperCase()) : null; 274 } 275 276 private Map<String, String> _configureMetadata (Configuration metadataConf) throws ConfigurationException 277 { 278 Map<String, String> metadata = new HashMap<>(); 279 280 this._metadataCondition = Condition.valueOf(metadataConf.getAttribute("type", "AND").toUpperCase()); 281 282 Configuration[] children = metadataConf.getChildren("metadata"); 283 for (Configuration child : children) 284 { 285 metadata.put(child.getAttribute("name"), child.getValue("")); 286 } 287 return metadata; 288 } 289 290 private Map<String, String> _configureProperties (Configuration propConf) throws ConfigurationException 291 { 292 Map<String, String> properties = new HashMap<>(); 293 294 this._propertiesCondition = Condition.valueOf(propConf.getAttribute("type", "AND").toUpperCase()); 295 296 Configuration[] children = propConf.getChildren("property"); 297 for (Configuration child : children) 298 { 299 properties.put(child.getAttribute("name"), child.getValue("")); 300 } 301 return properties; 302 } 303 304 private boolean _matchesTags (Page page) 305 { 306 List<String> tags = getTags(); 307 308 if (!tags.isEmpty()) 309 { 310 Condition condition = getTagsCondition(); 311 if (condition == Condition.AND) 312 { 313 return page.getTags().containsAll(tags); 314 } 315 else if (condition == Condition.OR) 316 { 317 for (String tagName : tags) 318 { 319 if (page.getTags().contains(tagName)) 320 { 321 return true; 322 } 323 } 324 return false; 325 } 326 } 327 return true; 328 } 329 330 private boolean _matchesMetadata (MetadataAwareSitemapElement page) 331 { 332 Map<String, String> metadata = getMetadata(); 333 if (!metadata.isEmpty()) 334 { 335 Condition condition = getMetadataCondition(); 336 if (condition == Condition.AND) 337 { 338 return _matchesAndData(metadata, page); 339 } 340 else if (condition == Condition.OR) 341 { 342 return _matchesOrData(metadata, page); 343 } 344 } 345 return true; 346 } 347 348 private boolean _matchesProperties (SitemapElement page) 349 { 350 Map<String, String> properties = getProperties(); 351 if (!properties.isEmpty()) 352 { 353 Condition condition = getMetadataCondition(); 354 if (condition == Condition.AND) 355 { 356 return _matchesAndProperties(properties, page); 357 } 358 else if (condition == Condition.OR) 359 { 360 return _matchesOrProperties(properties, page); 361 } 362 } 363 return true; 364 } 365 366 private boolean _matchesAndProperties (Map<String, String> properties, SitemapElement page) 367 { 368 if (!(page instanceof JCRAmetysObject)) 369 { 370 return properties.size() == 0; 371 } 372 373 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 374 Node node = jcrPage.getNode(); 375 376 try 377 { 378 for (String propertyName : properties.keySet()) 379 { 380 String valueToTest = properties.get(propertyName); 381 382 if (!node.hasProperty(propertyName)) 383 { 384 // property does not exits 385 return false; 386 } 387 388 Property property = node.getProperty(propertyName); 389 if (property.getDefinition().isMultiple()) 390 { 391 Value[] values = property.getValues(); 392 if (valueToTest.length() > 0 && values.length == 0) 393 { 394 // metadata exits but is empty 395 return false; 396 } 397 else if (valueToTest.length() > 0) 398 { 399 String[] results = new String[values.length]; 400 for (int i = 0; i < values.length; i++) 401 { 402 Value value = values[i]; 403 results[i] = value.getString(); 404 } 405 406 List<String> asList = Arrays.asList(results); 407 String[] valuesToTest = valueToTest.split(","); 408 for (String val : valuesToTest) 409 { 410 if (!asList.contains(val)) 411 { 412 // values do not contain all test values 413 return false; 414 } 415 } 416 } 417 } 418 else 419 { 420 String value = property.getValue().getString(); 421 if (valueToTest.length() > 0 && !valueToTest.equals(value)) 422 { 423 // value is not equals to test value 424 return false; 425 } 426 } 427 } 428 } 429 catch (RepositoryException e) 430 { 431 getLogger().error("An error occurred while testing properties", e); 432 return false; 433 } 434 return true; 435 } 436 437 private boolean _matchesOrProperties (Map<String, String> properties, SitemapElement page) 438 { 439 if (!(page instanceof JCRAmetysObject)) 440 { 441 return properties.size() == 0; 442 } 443 444 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 445 Node node = jcrPage.getNode(); 446 447 try 448 { 449 for (String propertyName : properties.keySet()) 450 { 451 String valueToTest = properties.get(propertyName); 452 453 if (node.hasProperty(propertyName)) 454 { 455 Property property = node.getProperty(propertyName); 456 if (property.getDefinition().isMultiple()) 457 { 458 Value[] values = property.getValues(); 459 if (valueToTest.length() == 0 && values.length > 0) 460 { 461 // multiple metadata exists and is not empty 462 return true; 463 } 464 else if (valueToTest.length() != 0) 465 { 466 String[] results = new String[values.length]; 467 for (int i = 0; i < values.length; i++) 468 { 469 Value value = values[i]; 470 results[i] = value.getString(); 471 } 472 473 List<String> asList = Arrays.asList(results); 474 String[] valuesToTest = valueToTest.split(","); 475 boolean findAll = true; 476 for (String val : valuesToTest) 477 { 478 if (!asList.contains(val)) 479 { 480 findAll = false; 481 } 482 } 483 if (findAll) 484 { 485 // values contain all test values 486 return true; 487 } 488 } 489 } 490 else 491 { 492 String value = property.getString(); 493 if (valueToTest.length() == 0 || valueToTest.equals(value)) 494 { 495 // value is equals to test value 496 return true; 497 } 498 } 499 } 500 } 501 } 502 catch (RepositoryException e) 503 { 504 getLogger().error("An error occurred while testing properties", e); 505 return false; 506 } 507 508 return false; 509 } 510 511 /** 512 * Check if page is live 513 * @param page the page to test 514 * @return true if page is live, else false 515 */ 516 private boolean _matchesLivePage(Page page) 517 { 518 Request request = ContextHelper.getRequest(_context); 519 String currentWp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 520 521 try 522 { 523 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "live"); 524 _resolver.resolveById(page.getId()); 525 } 526 catch (UnknownAmetysObjectException e) 527 { 528 return false; 529 } 530 finally 531 { 532 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWp); 533 } 534 return true; 535 } 536 537 @SuppressWarnings("unchecked") 538 private boolean _matchesAndData (Map<String, String> data, ModelLessDataHolder dataHolder) 539 { 540 for (String dataPath : data.keySet()) 541 { 542 if (!dataHolder.hasValueOrEmpty(dataPath)) 543 { 544 // data does not exits 545 return false; 546 } 547 548 Object value = dataHolder.getValue(dataPath); 549 550 if (value instanceof Composite) 551 { 552 if (((Composite) value).getDataNames().isEmpty()) 553 { 554 // the composite data is empty 555 return false; 556 } 557 } 558 else if (value instanceof Repeater) 559 { 560 if (((Repeater) value).getSize() <= 0) 561 { 562 // the repeater is empty 563 return false; 564 } 565 } 566 else 567 { 568 String valueToTest = data.get(dataPath); 569 if (!valueToTest.isEmpty() && !dataHolder.hasValue(dataPath)) 570 { 571 // data exits but is empty 572 return false; 573 } 574 else if (!valueToTest.isEmpty()) 575 { 576 ElementType type = (ElementType) dataHolder.getType(dataPath); 577 if (dataHolder.isMultiple(dataPath)) 578 { 579 if (!_findAllValuesInMultipleData(valueToTest, value, type)) 580 { 581 // value does not contain all test values 582 return false; 583 } 584 } 585 else 586 { 587 if (!valueToTest.equals(type.toString(value))) 588 { 589 // value is not equals to test value 590 return false; 591 } 592 } 593 } 594 } 595 } 596 597 // All data have matched 598 return true; 599 } 600 601 private boolean _matchesOrData (Map<String, String> data, ModelLessDataHolder dataHolder) 602 { 603 for (String dataPath : data.keySet()) 604 { 605 if (_matchesOrData(dataPath, data, dataHolder)) 606 { 607 return true; 608 } 609 } 610 611 return false; 612 } 613 614 @SuppressWarnings("unchecked") 615 private boolean _matchesOrData (String dataPath, Map<String, String> data, ModelLessDataHolder dataHolder) 616 { 617 if (!dataHolder.hasValueOrEmpty(dataPath)) 618 { 619 // data does not exists 620 return false; 621 } 622 623 Object value = dataHolder.getValue(dataPath); 624 625 if (value instanceof Composite) 626 { 627 if (!((Composite) value).getDataNames().isEmpty()) 628 { 629 // composite data is not empty, the condition matched 630 return true; 631 } 632 } 633 else if (value instanceof Repeater) 634 { 635 if (((Repeater) value).getSize() > 0) 636 { 637 // repeater data is not empty, the condition matched 638 return true; 639 } 640 } 641 else 642 { 643 String valueToTest = data.get(dataPath); 644 if (valueToTest.isEmpty()) 645 { 646 // No data to test 647 return true; 648 } 649 else if (dataHolder.hasValue(dataPath)) 650 { 651 ElementType type = (ElementType) dataHolder.getType(dataPath); 652 if (dataHolder.isMultiple(dataPath)) 653 { 654 if (_findAllValuesInMultipleData(valueToTest, value, type)) 655 { 656 // values contain all test values 657 return true; 658 } 659 } 660 else if (valueToTest.equals(type.toString(value))) 661 { 662 // value is equals to test value 663 return true; 664 } 665 } 666 } 667 668 return false; 669 } 670 671 private boolean _findAllValuesInMultipleData(String valueToTest, Object valueFromDataHolder, ElementType type) 672 { 673 List<String> valuesFromDataHolderAsString = new ArrayList<>(); 674 for (int i = 0; i < Array.getLength(valueFromDataHolder); i++) 675 { 676 @SuppressWarnings("unchecked") 677 String valueAsString = type.toString(Array.get(valueFromDataHolder, i)); 678 valuesFromDataHolderAsString.add(valueAsString); 679 } 680 681 String[] valuesToTest = valueToTest.split(","); 682 for (String value : valuesToTest) 683 { 684 if (!valuesFromDataHolderAsString.contains(value)) 685 { 686 return false; 687 } 688 } 689 690 return true; 691 } 692}