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