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