001/* 002 * Copyright 2019 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.inputdata; 017 018import java.util.HashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.stream.Collectors; 022 023import org.apache.avalon.framework.activity.Initializable; 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.cocoon.ProcessingException; 029import org.apache.cocoon.xml.AttributesImpl; 030import org.apache.cocoon.xml.XMLUtils; 031import org.apache.commons.lang.StringUtils; 032import org.apache.commons.lang3.tuple.Pair; 033import org.xml.sax.ContentHandler; 034import org.xml.sax.SAXException; 035 036import org.ametys.cms.tag.TagProviderExtensionPoint; 037import org.ametys.core.cache.AbstractCacheManager; 038import org.ametys.core.cache.Cache; 039import org.ametys.core.right.RightManager; 040import org.ametys.core.user.CurrentUserProvider; 041import org.ametys.core.user.UserIdentity; 042import org.ametys.core.util.SizeUtils; 043import org.ametys.core.util.SizeUtils.Sized; 044import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.repository.provider.WorkspaceSelector; 047import org.ametys.runtime.i18n.I18nizableText; 048import org.ametys.runtime.model.type.ElementType; 049import org.ametys.runtime.model.type.ModelItemType; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051import org.ametys.web.pageaccess.RestrictedPagePolicy; 052import org.ametys.web.renderingcontext.RenderingContext; 053import org.ametys.web.renderingcontext.RenderingContextHandler; 054import org.ametys.web.repository.page.Page; 055import org.ametys.web.repository.page.Page.PageType; 056import org.ametys.web.repository.site.Site; 057import org.ametys.web.repository.site.SiteManager; 058import org.ametys.web.repository.sitemap.Sitemap; 059 060/** 061 * Send a sitemap as SAX events. 062 * Handles both cacheable and non-cacheable cases. 063 */ 064public class SitemapSaxer extends AbstractLogEnabled implements Serviceable, Component, Initializable 065{ 066 /** Avalon role. */ 067 public static final String ROLE = SitemapSaxer.class.getName(); 068 069 /** Prefix for sitemap namespace. */ 070 public static final String NAMESPACE_PREFIX = "sitemap"; 071 /** URI for sitemap namespace. */ 072 public static final String NAMESPACE_URI = "http://www.ametys.org/inputdata/sitemap/3.0"; 073 074 static RightManager _rightManager; 075 static AmetysObjectResolver _resolver; 076 static TagProviderExtensionPoint _tagProviderExtensionPoint; 077 078 private RenderingContextHandler _renderingContextHandler; 079 private CurrentUserProvider _currentUserProvider; 080 private WorkspaceSelector _workspaceSelector; 081 private SiteManager _siteManager; 082 private AbstractCacheManager _cacheManager; 083 084 static class PageElementKey extends AbstractCacheKey 085 { 086 PageElementKey(String siteName, String workspace, String sitemap) 087 { 088 super(siteName, workspace, sitemap); 089 } 090 091 static PageElementKey of(String siteName, String workspace, String sitemap) 092 { 093 return new PageElementKey(siteName, workspace, sitemap); 094 } 095 096 static PageElementKey of(String siteName, String workspace) 097 { 098 return of(siteName, workspace, null); 099 } 100 } 101 102 static enum PageStatus 103 { 104 NOT_IN_PATH, 105 IN_PATH, 106 CURRENT, 107 DESCENDANT 108 } 109 110 @Override 111 public void service(ServiceManager manager) throws ServiceException 112 { 113 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 114 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 115 _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE); 116 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 117 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 118 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 119 _tagProviderExtensionPoint = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE); 120 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 121 } 122 123 @Override 124 public void initialize() throws Exception 125 { 126 _cacheManager.createMemoryCache(ROLE, 127 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MAP_SAXER_CACHE_LABEL"), 128 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MAP_SAXER_CACHE_DESCRIPTION"), 129 true, 130 null); 131 } 132 133 /** 134 * Clear the page cache. 135 * @param siteName the site. 136 * @param workspace the JCR workspace. 137 */ 138 public void clearCache(String siteName, String workspace) 139 { 140 _getCache().invalidate(PageElementKey.of(siteName, workspace)); 141 } 142 143 /** 144 * Send SAX events representing pages from a sitemap. 145 * @param contentHandler the SAX handler. 146 * @param siteName the site name. 147 * @param sitemapName the sitemap name. 148 * @param currentPage the current {@link Page}. 149 * @param initialDepth max depth from the root. 150 * @param descendantDepth max depth under the current page. 151 * @throws SAXException if an error occurs with the content handler. 152 * @throws ProcessingException if an error occurs while processing data. 153 */ 154 @SuppressWarnings("unchecked") 155 public void toSAX(ContentHandler contentHandler, String siteName, String sitemapName, Page currentPage, int initialDepth, int descendantDepth) throws SAXException, ProcessingException 156 { 157 RenderingContext renderingContext = _renderingContextHandler.getRenderingContext(); 158 String workspace = _workspaceSelector.getWorkspace(); 159 UserIdentity user = _currentUserProvider.getUser(); 160 161 Site site = _siteManager.getSite(siteName); 162 Sitemap sitemap = site.getSitemap(sitemapName); 163 RestrictedPagePolicy policy = site.getRestrictedPagePolicy(); 164 165 List<? extends PageWrapper> pages = null; 166 167 boolean useCache = policy == RestrictedPagePolicy.DISPLAYED; 168 PageElementKey cacheKey = PageElementKey.of(siteName, workspace, sitemapName); 169 170 if (!useCache) 171 { 172 // input data not cacheable, so no need to cache anything 173 pages = sitemap.getChildrenPages().stream() 174 .map(RepositoryPage::new) 175 .collect(Collectors.toList()); 176 } 177 else 178 { 179 // cache is filled incrementaly, so let's initialize it with the first level 180 pages = _getCache().get(cacheKey, __ -> sitemap.getChildrenPages().stream().map(BufferedPage::new).collect(Collectors.toList())); 181 } 182 183 contentHandler.startPrefixMapping(NAMESPACE_PREFIX, NAMESPACE_URI); 184 185 AttributesImpl attrs = new AttributesImpl(); 186 attrs.addCDATAAttribute(NAMESPACE_URI, "site", NAMESPACE_PREFIX + ":site", siteName); 187 attrs.addCDATAAttribute(NAMESPACE_URI, "lang", NAMESPACE_PREFIX + ":lang", sitemapName); 188 attrs.addCDATAAttribute(NAMESPACE_URI, "id", NAMESPACE_PREFIX + ":id", sitemap.getId()); 189 190 XMLUtils.startElement(contentHandler, "sitemap", attrs); 191 192 // Process recursively pages 193 boolean hasChanged = _saxPages(contentHandler, pages, currentPage, currentPage != null ? currentPage.getPathInSitemap() : null, user, renderingContext, site.getRestrictedPagePolicy(), initialDepth, descendantDepth, 1, -1); 194 195 if (useCache && hasChanged) 196 { 197 // force cache size computing by inserting again the pages 198 _getCache().put(cacheKey, (List<BufferedPage>) pages); 199 } 200 201 XMLUtils.endElement(contentHandler, "sitemap"); 202 contentHandler.endPrefixMapping(NAMESPACE_PREFIX); 203 } 204 205 private boolean _saxPages(ContentHandler handler, List<? extends PageWrapper> pages, Page currentPage, String currentPagePath, UserIdentity userIdentity, RenderingContext renderingContext, RestrictedPagePolicy policy, int initialDepth, int descendantDepth, int currentDepth, int currentDescendantDepth) throws SAXException 206 { 207 boolean inBackOffice = renderingContext == RenderingContext.BACK || renderingContext == RenderingContext.PREVIEW; 208 boolean hasChanged = false; 209 210 for (PageWrapper page : pages) 211 { 212 String pagePath = page.getPath(); 213 214 if (inBackOffice || policy == RestrictedPagePolicy.DISPLAYED || page.hasReadAccess(userIdentity)) 215 { 216 if (page.isVisible() || StringUtils.isNotBlank(currentPagePath) && currentPagePath.startsWith(pagePath)) 217 { 218 boolean hasChangedInPage = _saxPage(handler, page, currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth, currentDescendantDepth); 219 hasChanged = hasChanged || hasChangedInPage; 220 } 221 } 222 } 223 224 return hasChanged; 225 } 226 227 private boolean _saxPage(ContentHandler handler, PageWrapper page, Page currentPage, String currentPagePath, UserIdentity userIdentity, RenderingContext renderingContext, RestrictedPagePolicy policy, int initialDepth, int descendantDepth, int currentDepth, int currentDescendantDepth) throws SAXException 228 { 229 String pagePath = page.getPath(); 230 231 AttributesImpl attrs = new AttributesImpl(); 232 Map<String, String> internalAttributes = page.getInternalAttributes(); 233 234 for (String name : internalAttributes.keySet()) 235 { 236 attrs.addCDATAAttribute(NAMESPACE_URI, name, NAMESPACE_PREFIX + ':' + name, internalAttributes.get(name)); 237 } 238 239 Map<String, String> attributes = page.getAttributes(); 240 241 for (String name : attributes.keySet()) 242 { 243 attrs.addCDATAAttribute(name, attributes.get(name)); 244 } 245 246 PageStatus pageStatus = _saxCurrentStatus(currentPagePath, pagePath, attrs); 247 248 boolean hasChanged = false; 249 250 XMLUtils.startElement(handler, "page", attrs); 251 252 // Continue if current depth is less than initial depth 253 // or if we are inside current path or in current page descendants 254 if (currentDepth <= initialDepth || pageStatus == PageStatus.IN_PATH || pageStatus == PageStatus.CURRENT && descendantDepth > 0 || pageStatus == PageStatus.DESCENDANT && currentDescendantDepth < descendantDepth) 255 { 256 int descendant = (pageStatus == PageStatus.CURRENT) ? 1 : (pageStatus == PageStatus.DESCENDANT ? currentDescendantDepth + 1 : -1); 257 Pair<List< ? extends PageWrapper>, Boolean> children = page.getChildren(); 258 boolean hasChangedInPage = children.getRight(); 259 260 boolean hasChangedInChildren = _saxPages(handler, children.getLeft(), currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth + 1, descendant); 261 262 hasChanged = hasChangedInPage || hasChangedInChildren; 263 } 264 265 XMLUtils.endElement(handler, "page"); 266 267 return hasChanged; 268 } 269 270 /** 271 * SAX current status. 272 * @param currentPagePath the path to the current page. 273 * @param pagePath the path to the page to process. 274 * @param attrs the attributes to populate. 275 * @return the current status. 276 */ 277 protected PageStatus _saxCurrentStatus(String currentPagePath, String pagePath, AttributesImpl attrs) 278 { 279 PageStatus result = PageStatus.NOT_IN_PATH; 280 281 if (currentPagePath == null) 282 { 283 return PageStatus.NOT_IN_PATH; 284 } 285 286 // If the page in an ancestor of the current page 287 boolean isPageCurrent = currentPagePath.equals(pagePath); 288 289 if (currentPagePath.startsWith(pagePath + "/") || isPageCurrent) 290 { 291 attrs.addCDATAAttribute(NAMESPACE_URI, "in-path", NAMESPACE_PREFIX + ":in-path", "true"); 292 result = PageStatus.IN_PATH; 293 } 294 else if (pagePath.startsWith(currentPagePath)) 295 { 296 result = PageStatus.DESCENDANT; 297 } 298 299 // If this is the current page 300 if (isPageCurrent) 301 { 302 attrs.addCDATAAttribute(NAMESPACE_URI, "current", NAMESPACE_PREFIX + ":current", "true"); 303 result = PageStatus.CURRENT; 304 } 305 306 return result; 307 } 308 309 interface PageWrapper 310 { 311 boolean isVisible(); 312 String getPath(); 313 Map<String, String> getAttributes(); 314 Map<String, String> getInternalAttributes(); 315 boolean hasReadAccess(UserIdentity user); 316 317 // actual children + a boolean indicating newly-fetched children 318 Pair<List<? extends PageWrapper>, Boolean> getChildren(); 319 320 default Map<String, String> _getInternalAttributes(Page page) 321 { 322 Map<String, String> internalAttributes = new HashMap<>(); 323 String path = page.getPathInSitemap(); 324 PageType pageType = page.getType(); 325 internalAttributes.put("id", page.getId()); 326 internalAttributes.put("name", page.getName()); 327 internalAttributes.put("title", page.getTitle()); 328 internalAttributes.put("long-title", page.getLongTitle()); 329 internalAttributes.put("path", path); 330 internalAttributes.put("type", page.getType().name()); 331 332 if (!page.isVisible()) 333 { 334 internalAttributes.put("invisible", "true"); 335 } 336 337 if (!_rightManager.hasAnonymousReadAccess(page)) 338 { 339 internalAttributes.put("restricted", "true"); 340 } 341 342 // Add a flag if there is data 343 if (pageType == PageType.CONTAINER) 344 { 345 internalAttributes.put("container", "true"); 346 } 347 // Add URL if found 348 else if (pageType == PageType.LINK) 349 { 350 internalAttributes.put("link", page.getURL()); 351 internalAttributes.put("link-type", page.getURLType().name()); 352 } 353 354 return internalAttributes; 355 } 356 357 @SuppressWarnings("unchecked") 358 default Map<String, String> _getAttributes(Page page) 359 { 360 Map<String, String> attributes = new HashMap<>(); 361 362 for (String dataName : page.getDataNames()) 363 { 364 // SAX only non composite and non multivalued data 365 ModelItemType type = page.getType(dataName); 366 if (type instanceof ElementType && !page.isMultiple(dataName)) 367 { 368 Object value = page.getValue(dataName); 369 attributes.put(dataName, ((ElementType) type).toString(value)); 370 } 371 } 372 373 // tags 374 for (String tag : page.getTags()) 375 { 376 Map<String, Object> params = Map.of("siteName", page.getSiteName()); 377 if (_tagProviderExtensionPoint.hasTag(tag, params)) 378 { 379 attributes.put("PLUGIN_TAGS_" + tag, "empty"); 380 } 381 } 382 383 return attributes; 384 } 385 } 386 387 static class BufferedPage implements PageWrapper, Sized 388 { 389 String _path; 390 Map<String, String> _attributes; 391 Map<String, String> _internalAttributes; 392 boolean _visible = true; 393 boolean _childrenFetched; 394 long _size = -1; 395 396 List<BufferedPage> _children; 397 398 BufferedPage(Page page) 399 { 400 _path = page.getPathInSitemap(); 401 402 Map<String, String> internalAttributes = _getInternalAttributes(page); 403 _internalAttributes = internalAttributes; 404 if ("true".equals(internalAttributes.get("invisible"))) 405 { 406 _visible = false; 407 } 408 409 _attributes = _getAttributes(page); 410 } 411 412 public long getSize() 413 { 414 if (_size == -1) 415 { 416 _size = SizeUtils.shallowSizeOf(this) + SizeUtils.totalSizeOf(_path, _attributes, _internalAttributes); 417 } 418 419 long size = _size; 420 421 if (_children != null) 422 { 423 for (BufferedPage child : _children) 424 { 425 size += child.getSize(); 426 } 427 } 428 429 return size; 430 } 431 432 @Override 433 public Pair<List<? extends PageWrapper>, Boolean> getChildren() 434 { 435 boolean hasChanged = false; 436 437 if (!_childrenFetched) 438 { 439 // fill cache when needed 440 String pageId = _internalAttributes.get("id"); 441 Page page = _resolver.resolveById(pageId); 442 443 _children = page.getChildrenPages().stream().map(BufferedPage::new).collect(Collectors.toList()); 444 _childrenFetched = true; 445 hasChanged = true; 446 } 447 448 return Pair.of(_children, hasChanged); 449 } 450 451 @Override 452 public boolean isVisible() 453 { 454 return _visible; 455 } 456 457 @Override 458 public String getPath() 459 { 460 return _path; 461 } 462 463 @Override 464 public Map<String, String> getAttributes() 465 { 466 return _attributes; 467 } 468 469 @Override 470 public Map<String, String> getInternalAttributes() 471 { 472 return _internalAttributes; 473 } 474 475 @Override 476 public boolean hasReadAccess(UserIdentity user) 477 { 478 throw new UnsupportedOperationException("A BufferedPage has no concept of read access"); 479 } 480 } 481 482 class RepositoryPage implements PageWrapper 483 { 484 Page _page; 485 486 RepositoryPage(Page page) 487 { 488 _page = page; 489 } 490 491 @Override 492 public Pair<List<? extends PageWrapper>, Boolean> getChildren() 493 { 494 return Pair.of(_page.getChildrenPages().stream().map(RepositoryPage::new).collect(Collectors.toList()), false); 495 } 496 497 @Override 498 public boolean isVisible() 499 { 500 return _page.isVisible(); 501 } 502 503 @Override 504 public String getPath() 505 { 506 return _page.getPathInSitemap(); 507 } 508 509 @Override 510 public Map<String, String> getAttributes() 511 { 512 return _getAttributes(_page); 513 } 514 515 @Override 516 public Map<String, String> getInternalAttributes() 517 { 518 return _getInternalAttributes(_page); 519 } 520 521 @Override 522 public boolean hasReadAccess(UserIdentity user) 523 { 524 return _rightManager.hasReadAccess(user, _page); 525 } 526 } 527 528 private Cache<PageElementKey, List<BufferedPage>> _getCache() 529 { 530 return _cacheManager.get(ROLE); 531 } 532}