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.ConcurrentHashMap; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.logger.AbstractLogEnabled; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.cocoon.ProcessingException; 031import org.apache.cocoon.xml.AttributesImpl; 032import org.apache.cocoon.xml.XMLUtils; 033import org.apache.commons.lang.StringUtils; 034import org.xml.sax.ContentHandler; 035import org.xml.sax.SAXException; 036 037import org.ametys.cms.data.type.ModelItemTypeConstants; 038import org.ametys.core.right.RightManager; 039import org.ametys.core.user.CurrentUserProvider; 040import org.ametys.core.user.UserIdentity; 041import org.ametys.plugins.repository.AmetysObjectIterable; 042import org.ametys.plugins.repository.provider.WorkspaceSelector; 043import org.ametys.runtime.model.type.ElementType; 044import org.ametys.runtime.model.type.ModelItemType; 045import org.ametys.web.pageaccess.RestrictedPagePolicy; 046import org.ametys.web.renderingcontext.RenderingContext; 047import org.ametys.web.renderingcontext.RenderingContextHandler; 048import org.ametys.web.repository.page.Page; 049import org.ametys.web.repository.page.Page.PageType; 050import org.ametys.web.repository.site.Site; 051import org.ametys.web.repository.site.SiteManager; 052import org.ametys.web.repository.sitemap.Sitemap; 053 054/** 055 * Send a sitemap as SAX events. 056 * Handles both cacheable and non-cacheable cases. 057 */ 058public class SitemapSaxer extends AbstractLogEnabled implements Serviceable, Component 059{ 060 /** Avalon role. */ 061 public static final String ROLE = SitemapSaxer.class.getName(); 062 063 /** Prefix for sitemap namespace. */ 064 public static final String NAMESPACE_PREFIX = "sitemap"; 065 /** URI for sitemap namespace. */ 066 public static final String NAMESPACE_URI = "http://www.ametys.org/inputdata/sitemap/3.0"; 067 068 // Constants for current path management 069 private static final int PATH_DESCENDANT = -2; 070 private static final int PATH_NOT_IN_PATH = -1; 071 private static final int PATH_IN_PATH = 0; 072 private static final int PATH_CURRENT = 1; 073 074 RightManager _rightManager; 075 private RenderingContextHandler _renderingContextHandler; 076 private CurrentUserProvider _currentUserProvider; 077 private WorkspaceSelector _workspaceSelector; 078 private SiteManager _siteManager; 079 080 // sitename workspace sitemap renderingcontext pages 081 private Map<String, Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>>> _bufferedPages = new ConcurrentHashMap<>(); 082 083 @Override 084 public void service(ServiceManager manager) throws ServiceException 085 { 086 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 087 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 088 _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE); 089 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 090 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 091 } 092 093 /** 094 * Clear the page cache. 095 * @param siteName the site. 096 * @param workspace the JCR workspace. 097 */ 098 public void clearCache(String siteName, String workspace) 099 { 100 Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>> sitePages = _bufferedPages.get(siteName); 101 if (sitePages != null) 102 { 103 Map<String, Map<RenderingContext, List<BufferedPage>>> workspacePages = sitePages.get(workspace); 104 if (workspacePages != null) 105 { 106 workspacePages.clear(); 107 } 108 } 109 } 110 111 /** 112 * Send SAX events representing pages from a sitemap. 113 * @param contentHandler the SAX handler. 114 * @param siteName the site name. 115 * @param sitemapName the sitemap name. 116 * @param currentPage the current {@link Page}. 117 * @param initialDepth max depth from the root. 118 * @param descendantDepth max depth under the current page. 119 * @throws SAXException if an error occurs with the content handler. 120 * @throws ProcessingException if an error occurs while processing data. 121 */ 122 public void toSAX(ContentHandler contentHandler, String siteName, String sitemapName, Page currentPage, int initialDepth, int descendantDepth) throws SAXException, ProcessingException 123 { 124 RenderingContext renderingContext = _renderingContextHandler.getRenderingContext(); 125 String workspace = _workspaceSelector.getWorkspace(); 126 UserIdentity user = _currentUserProvider.getUser(); 127 128 Site site = _siteManager.getSite(siteName); 129 Sitemap sitemap = site.getSitemap(sitemapName); 130 RestrictedPagePolicy policy = site.getRestrictedPagePolicy(); 131 132 List<? extends PageWrapper> pages = null; 133 134 if (policy == RestrictedPagePolicy.HIDDEN) 135 { 136 // input data not cacheable, so no need to cache anything 137 pages = sitemap.getChildrenPages().stream().map(RepositoryPage::new).collect(Collectors.toList()); 138 } 139 else 140 { 141 // input data is cacheable, retrive data from cache, or fill the cache 142 Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>> sitePages = _bufferedPages.computeIfAbsent(siteName, s -> new ConcurrentHashMap<>()); 143 Map<String, Map<RenderingContext, List<BufferedPage>>> workspacePages = sitePages.computeIfAbsent(workspace, w -> new ConcurrentHashMap<>()); 144 Map<RenderingContext, List<BufferedPage>> sitemapPages = workspacePages.computeIfAbsent(sitemapName, s -> new ConcurrentHashMap<>()); 145 146 pages = sitemapPages.computeIfAbsent(renderingContext, r -> _fill(sitemap.getChildrenPages())); 147 } 148 149 contentHandler.startPrefixMapping(NAMESPACE_PREFIX, NAMESPACE_URI); 150 151 AttributesImpl attrs = new AttributesImpl(); 152 attrs.addCDATAAttribute(NAMESPACE_URI, "site", NAMESPACE_PREFIX + ":site", siteName); 153 attrs.addCDATAAttribute(NAMESPACE_URI, "lang", NAMESPACE_PREFIX + ":lang", sitemapName); 154 attrs.addCDATAAttribute(NAMESPACE_URI, "id", NAMESPACE_PREFIX + ":id", sitemap.getId()); 155 156 XMLUtils.startElement(contentHandler, "sitemap", attrs); 157 158 // Process recursively pages 159 _saxPages(contentHandler, pages, currentPage, currentPage != null ? currentPage.getPathInSitemap() : null, user, renderingContext, site.getRestrictedPagePolicy(), initialDepth, descendantDepth, 1, -1); 160 161 XMLUtils.endElement(contentHandler, "sitemap"); 162 contentHandler.endPrefixMapping(NAMESPACE_PREFIX); 163 } 164 165 private List<BufferedPage> _fill(AmetysObjectIterable<? extends Page> pages) 166 { 167 List<BufferedPage> result = new ArrayList<>(); 168 169 for (Page page : pages) 170 { 171 BufferedPage bufferedPage = new BufferedPage(); 172 173 bufferedPage._path = page.getPathInSitemap(); 174 175 Map<String, String> internalAttributes = _getInternalAttributes(page); 176 bufferedPage._internalAttributes = internalAttributes; 177 if ("true".equals(internalAttributes.get("invisible"))) 178 { 179 bufferedPage._visible = false; 180 } 181 182 bufferedPage._attributes = _getAttributes(page); 183 184 bufferedPage._children = _fill(page.getChildrenPages()); 185 result.add(bufferedPage); 186 } 187 188 return result; 189 } 190 191 Map<String, String> _getInternalAttributes(Page page) 192 { 193 Map<String, String> internalAttributes = new HashMap<>(); 194 String path = page.getPathInSitemap(); 195 PageType pageType = page.getType(); 196 internalAttributes.put("id", page.getId()); 197 internalAttributes.put("name", page.getName()); 198 internalAttributes.put("title", page.getTitle()); 199 internalAttributes.put("long-title", page.getLongTitle()); 200 internalAttributes.put("path", path); 201 internalAttributes.put("type", page.getType().name()); 202 203 if (!page.isVisible()) 204 { 205 internalAttributes.put("invisible", "true"); 206 } 207 208 if (!_rightManager.hasAnonymousReadAccess(page)) 209 { 210 internalAttributes.put("restricted", "true"); 211 } 212 213 // Add a flag if there is data 214 if (pageType == PageType.CONTAINER) 215 { 216 internalAttributes.put("container", "true"); 217 } 218 // Add URL if found 219 else if (pageType == PageType.LINK) 220 { 221 internalAttributes.put("link", page.getURL()); 222 internalAttributes.put("link-type", page.getURLType().name()); 223 } 224 225 return internalAttributes; 226 } 227 228 @SuppressWarnings("unchecked") 229 Map<String, String> _getAttributes(Page page) 230 { 231 Map<String, String> attributes = new HashMap<>(); 232 233 for (String metadataName : page.getDataNames()) 234 { 235 ModelItemType type = page.getType(metadataName); 236 237 // SAX only non composite, non binary, non rich text and non multivalued metadatas 238 if (!page.isMultiple(metadataName) && type instanceof ElementType) 239 { 240 ElementType elementType = (ElementType) type; 241 if (elementType.isSimple() || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(elementType.getId())) 242 { 243 Object value = page.getValue(metadataName); 244 attributes.put(metadataName, ((ElementType) type).toString(value)); 245 } 246 } 247 } 248 249 // tags 250 for (String tag : page.getTags()) 251 { 252 attributes.put("PLUGIN_TAGS_" + tag, "empty"); 253 } 254 255 return attributes; 256 } 257 258 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 259 { 260 boolean inBackOffice = renderingContext == RenderingContext.BACK || renderingContext == RenderingContext.PREVIEW; 261 262 for (PageWrapper page : pages) 263 { 264 String pagePath = page.getPath(); 265 266 if (inBackOffice || policy == RestrictedPagePolicy.DISPLAYED || page.hasReadAccess(userIdentity)) 267 { 268 if (page.isVisible() || (StringUtils.isNotBlank(currentPagePath) && currentPagePath.startsWith(pagePath))) 269 { 270 _saxPage(handler, page, currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth, currentDescendantDepth); 271 } 272 } 273 } 274 } 275 276 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 277 { 278 String pagePath = page.getPath(); 279 280 AttributesImpl attrs = new AttributesImpl(); 281 Map<String, String> internalAttributes = page.getInternalAttributes(); 282 283 for (String name : internalAttributes.keySet()) 284 { 285 attrs.addCDATAAttribute(NAMESPACE_URI, name, NAMESPACE_PREFIX + ':' + name, internalAttributes.get(name)); 286 } 287 288 Map<String, String> attributes = page.getAttributes(); 289 290 for (String name : attributes.keySet()) 291 { 292 attrs.addCDATAAttribute(name, attributes.get(name)); 293 } 294 295 int inPath = _saxCurrentStatus(currentPagePath, pagePath, attrs); 296 297 XMLUtils.startElement(handler, "page", attrs); 298 299 // Continue if current depth is less than initial depth 300 // or if we are inside current path or in current page descendants 301 if (currentDepth <= initialDepth || inPath >= PATH_IN_PATH || (inPath == PATH_DESCENDANT && currentDescendantDepth <= descendantDepth)) 302 { 303 int descendant = (inPath == PATH_CURRENT) ? 0 : (inPath == PATH_DESCENDANT ? currentDescendantDepth + 1 : -1); 304 _saxPages(handler, page.getChildren(), currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth + 1, descendant); 305 } 306 307 XMLUtils.endElement(handler, "page"); 308 } 309 310 /** 311 * SAX current status. 312 * @param currentPagePath the path to the current page. 313 * @param pagePath the path to the page to process. 314 * @param attrs the attributes to populate. 315 * @return the current status. 316 */ 317 protected int _saxCurrentStatus(String currentPagePath, String pagePath, AttributesImpl attrs) 318 { 319 int result = PATH_NOT_IN_PATH; 320 321 if (currentPagePath == null) 322 { 323 return PATH_NOT_IN_PATH; 324 } 325 326 // If the page in an ancestor of the current page 327 boolean isPageCurrent = currentPagePath.equals(pagePath); 328 329 if (currentPagePath.startsWith(pagePath + "/") || isPageCurrent) 330 { 331 attrs.addCDATAAttribute(NAMESPACE_URI, "in-path", NAMESPACE_PREFIX + ":in-path", "true"); 332 result = PATH_IN_PATH; 333 } 334 else if (pagePath.startsWith(currentPagePath)) 335 { 336 result = PATH_DESCENDANT; 337 } 338 339 // If this is the current page 340 if (isPageCurrent) 341 { 342 attrs.addCDATAAttribute(NAMESPACE_URI, "current", NAMESPACE_PREFIX + ":current", "true"); 343 result = PATH_CURRENT; 344 } 345 346 return result; 347 } 348 349 interface PageWrapper 350 { 351 List<? extends PageWrapper> getChildren(); 352 boolean isVisible(); 353 String getPath(); 354 Map<String, String> getAttributes(); 355 Map<String, String> getInternalAttributes(); 356 boolean hasReadAccess(UserIdentity user); 357 } 358 359 class BufferedPage implements PageWrapper 360 { 361 String _path; 362 Map<String, String> _attributes; 363 Map<String, String> _internalAttributes; 364 List<BufferedPage> _children; 365 boolean _visible = true; 366 367 public List<? extends PageWrapper> getChildren() 368 { 369 return _children; 370 } 371 372 public boolean isVisible() 373 { 374 return _visible; 375 } 376 377 public String getPath() 378 { 379 return _path; 380 } 381 382 public Map<String, String> getAttributes() 383 { 384 return _attributes; 385 } 386 387 public Map<String, String> getInternalAttributes() 388 { 389 return _internalAttributes; 390 } 391 392 public boolean hasReadAccess(UserIdentity user) 393 { 394 throw new UnsupportedOperationException("A BufferedPage has no concept of read access"); 395 } 396 } 397 398 class RepositoryPage implements PageWrapper 399 { 400 Page _page; 401 402 RepositoryPage(Page page) 403 { 404 _page = page; 405 } 406 407 public List<? extends PageWrapper> getChildren() 408 { 409 return _page.getChildrenPages().stream().map(RepositoryPage::new).collect(Collectors.toList()); 410 } 411 412 public boolean isVisible() 413 { 414 return _page.isVisible(); 415 } 416 417 public String getPath() 418 { 419 return _page.getPathInSitemap(); 420 } 421 422 public Map<String, String> getAttributes() 423 { 424 return _getAttributes(_page); 425 } 426 427 public Map<String, String> getInternalAttributes() 428 { 429 return _getInternalAttributes(_page); 430 } 431 432 public boolean hasReadAccess(UserIdentity user) 433 { 434 return _rightManager.hasReadAccess(user, _page); 435 } 436 } 437}