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