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