001/* 002 * Copyright 2016 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.plugins.userdirectory; 017 018import java.text.Normalizer; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Objects; 026import java.util.Set; 027import java.util.SortedSet; 028import java.util.TreeSet; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.activity.Initializable; 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.Strings; 040 041import org.ametys.cms.contenttype.ContentType; 042import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.repository.ContentTypeExpression; 045import org.ametys.cms.repository.LanguageExpression; 046import org.ametys.core.cache.AbstractCacheManager; 047import org.ametys.core.cache.Cache; 048import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 049import org.ametys.plugins.repository.AmetysObjectIterable; 050import org.ametys.plugins.repository.AmetysObjectResolver; 051import org.ametys.plugins.repository.AmetysRepositoryException; 052import org.ametys.plugins.repository.UnknownAmetysObjectException; 053import org.ametys.plugins.repository.provider.WorkspaceSelector; 054import org.ametys.plugins.repository.query.QueryHelper; 055import org.ametys.plugins.repository.query.SortCriteria; 056import org.ametys.plugins.repository.query.expression.AndExpression; 057import org.ametys.plugins.repository.query.expression.Expression; 058import org.ametys.plugins.repository.query.expression.Expression.Operator; 059import org.ametys.plugins.repository.query.expression.OrExpression; 060import org.ametys.plugins.repository.query.expression.StringExpression; 061import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression; 062import org.ametys.plugins.userdirectory.page.VirtualUserDirectoryPageFactory; 063import org.ametys.runtime.i18n.I18nizableText; 064import org.ametys.runtime.plugin.component.AbstractLogEnabled; 065import org.ametys.web.repository.page.Page; 066import org.ametys.web.repository.page.PageQueryHelper; 067 068/** 069 * Component providing methods to retrieve user directory virtual pages, such as the user directory root, 070 * transitional page and user page. 071 */ 072public class UserDirectoryPageHandler extends AbstractLogEnabled implements Component, Serviceable, Initializable 073{ 074 /** The avalon role. */ 075 public static final String ROLE = UserDirectoryPageHandler.class.getName(); 076 077 /** The data name for the content type of the user directory */ 078 public static final String CONTENT_TYPE_DATA_NAME = "user-directory-root-contenttype"; 079 /** The data name for the users' view to use */ 080 public static final String USER_VIEW_NAME = "user-directory-root-view-name"; 081 /** The data name for the classification attribute of the user directory */ 082 public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "user-directory-root-classification-metadata"; 083 /** The data name for the depth of the user directory */ 084 public static final String DEPTH_DATA_NAME = "user-directory-root-depth"; 085 /** The user directory root pages cache id */ 086 protected static final String ROOT_PAGES_CACHE = UserDirectoryPageHandler.class.getName() + "$rootPageIds"; 087 /** The user directory user pages cache id */ 088 protected static final String UD_PAGES_CACHE = UserDirectoryPageHandler.class.getName() + "$udPages"; 089 090 /** The workspace selector. */ 091 protected WorkspaceSelector _workspaceSelector; 092 /** The ametys object resolver. */ 093 protected AmetysObjectResolver _resolver; 094 /** The extension point for content types */ 095 protected ContentTypeExtensionPoint _contentTypeEP; 096 /** The cache manager */ 097 protected AbstractCacheManager _abstractCacheManager; 098 099 @Override 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 103 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 104 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 105 _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 106 } 107 108 @Override 109 public void initialize() throws Exception 110 { 111 _abstractCacheManager.createMemoryCache(ROOT_PAGES_CACHE, 112 new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_ROOT_PAGES_LABEL"), 113 new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_ROOT_PAGES_DESCRIPTION"), 114 true, 115 null); 116 _abstractCacheManager.createMemoryCache(UD_PAGES_CACHE, 117 new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_UD_PAGES_LABEL"), 118 new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_UD_PAGES_DESCRIPTION"), 119 true, 120 null); 121 } 122 123 /** 124 * Gets the user directory root pages from the given content type id, whatever the site. 125 * @param contentTypeId The content type id 126 * @return the user directory root pages. 127 * @throws AmetysRepositoryException if an error occured. 128 */ 129 public Set<Page> getUserDirectoryRootPages(String contentTypeId) throws AmetysRepositoryException 130 { 131 Expression expression = new VirtualFactoryExpression(VirtualUserDirectoryPageFactory.class.getName()); 132 Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId); 133 134 AndExpression andExp = new AndExpression(expression, contentTypeExp); 135 136 String query = PageQueryHelper.getPageXPathQuery(null, null, null, andExp, null); 137 138 AmetysObjectIterable<Page> pages = _resolver.query(query); 139 140 return pages.stream().collect(Collectors.toSet()); 141 } 142 143 /** 144 * Gets the user directory root page of a specific content type. 145 * @param siteName The site name 146 * @param sitemapName The sitemap 147 * @param contentTypeId The content type id 148 * @return the user directory root pages. 149 * @throws AmetysRepositoryException if an error occured. 150 */ 151 public Page getUserDirectoryRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException 152 { 153 String contentTypeIdToCompare = contentTypeId != null ? contentTypeId : ""; 154 155 for (Page userDirectoryRootPage : getUserDirectoryRootPages(siteName, sitemapName)) 156 { 157 if (contentTypeIdToCompare.equals(getContentTypeId(userDirectoryRootPage))) 158 { 159 return userDirectoryRootPage; 160 } 161 } 162 163 return null; 164 } 165 166 /** 167 * Gets the user directory root pages. 168 * @param siteName The site name 169 * @param sitemapName The sitemap 170 * @return the user directory root pages. 171 * @throws AmetysRepositoryException if an error occured. 172 */ 173 public Set<Page> getUserDirectoryRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 174 { 175 Set<Page> rootPages = new HashSet<>(); 176 177 String workspace = _workspaceSelector.getWorkspace(); 178 179 Cache<RootPageCacheKey, Set<String>> cache = getRootPagesCache(); 180 181 RootPageCacheKey key = RootPageCacheKey.of(workspace, siteName, sitemapName); 182 if (cache.hasKey(key)) 183 { 184 rootPages = cache.get(key).stream() 185 .map(this::_resolvePage) 186 .filter(Objects::nonNull) 187 .collect(Collectors.toSet()); 188 } 189 else 190 { 191 rootPages = _getUserDirectoryRootPages(siteName, sitemapName); 192 Set<String> userDirectoryRootPageIds = rootPages.stream() 193 .map(Page::getId) 194 .collect(Collectors.toSet()); 195 cache.put(key, userDirectoryRootPageIds); 196 } 197 198 return rootPages; 199 } 200 201 private Page _resolvePage(String pageId) 202 { 203 try 204 { 205 return _resolver.resolveById(pageId); 206 } 207 catch (UnknownAmetysObjectException e) 208 { 209 // The page stored in cache may have been deleted 210 return null; 211 } 212 } 213 214 /** 215 * Get the user directory root pages, without searching in the cache. 216 * @param siteName the current site. 217 * @param sitemapName the sitemap name. 218 * @return the user directory root pages 219 * @throws AmetysRepositoryException if an error occured. 220 */ 221 protected Set<Page> _getUserDirectoryRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 222 { 223 Expression expression = new VirtualFactoryExpression(VirtualUserDirectoryPageFactory.class.getName()); 224 225 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 226 227 AmetysObjectIterable<Page> pages = _resolver.query(query); 228 229 return pages.stream().collect(Collectors.toSet()); 230 } 231 232 /** 233 * Gets the depth of the user directory root page 234 * @param rootPage The user directory root page 235 * @return the depth of the user directory root page 236 */ 237 public int getDepth(Page rootPage) 238 { 239 return Math.toIntExact(rootPage.getValue(DEPTH_DATA_NAME)); 240 } 241 242 /** 243 * Gets the name of the classification attribute 244 * @param rootPage The user directory root page 245 * @return the name of the classification attribute 246 */ 247 public String getClassificationAttribute(Page rootPage) 248 { 249 return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME); 250 } 251 252 /** 253 * Gets the content type id 254 * @param rootPage The user directory root page 255 * @return the content type id 256 */ 257 public String getContentTypeId(Page rootPage) 258 { 259 return rootPage.getValue(CONTENT_TYPE_DATA_NAME); 260 } 261 262 /** 263 * Gets the content type 264 * @param rootPage The user directory root page 265 * @return the content type 266 */ 267 public ContentType getContentType(Page rootPage) 268 { 269 String contentTypeId = getContentTypeId(rootPage); 270 return StringUtils.isNotBlank(contentTypeId) ? _contentTypeEP.getExtension(contentTypeId) : null; 271 } 272 273 /** 274 * Gets the value of the classification attribute for the given content, transformed for building tree hierarchy 275 * <br>The transformation takes the lower-case of all characters, removes non-alphanumeric characters, 276 * and takes the first characters to not have a string with a size bigger than the depth 277 * <br>For instance, if the value for the content is "Aéa Foo-bar" and the depth is 7, 278 * then this method will return "aeafoob" 279 * @param rootPage The user directory root page 280 * @param content The content 281 * @return the transformed value of the classification attribute for the given content. Can be null 282 */ 283 public String getTransformedClassificationValue(Page rootPage, Content content) 284 { 285 String attribute = getClassificationAttribute(rootPage); 286 int depth = getDepth(rootPage); 287 288 // 1) get value of the classification attribute 289 String classification = content.getValue(attribute); 290 291 if (classification == null) 292 { 293 // The classification does not exists for the content 294 getLogger().info("The classification attribute '{}' does not exist for the content {}", attribute, content); 295 return null; 296 } 297 298 try 299 { 300 // 2) replace special character 301 // 3) remove '-' characters 302 303 // FIXME CMS-5758 FilterNameHelper.filterName do not authorized name with numbers only. 304 // So code of FilterNamehelper is temporarily duplicated here with a slightly modified RegExp 305// String transformedValue = FilterNameHelper.filterName(classification).replace("-", ""); 306 String transformedValue = _filterName(classification).replace("-", ""); 307 308 // 4) only keep 'depth' first characters (if depth = 3, "de" becomes "de", "debu" becomes "deb", etc.) 309 return StringUtils.substring(transformedValue, 0, depth); 310 } 311 catch (IllegalArgumentException e) 312 { 313 // The value of the classification attribute is not valid 314 getLogger().warn("The classification attribute '{}' does not have a valid value ({}) for the content {}", attribute, classification, content); 315 return null; 316 } 317 } 318 319 private String _filterName(String name) 320 { 321 Pattern pattern = Pattern.compile("^()[0-9-_]*[a-z0-9].*$"); 322 // Use lower case 323 // then remove accents 324 // then replace contiguous spaces with one dash 325 // and finally remove non-alphanumeric characters except - 326 String filteredName = Normalizer.normalize(name.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 327 filteredName = filteredName.replaceAll("œ", "oe").replaceAll("æ", "ae").replaceAll(" +", "-").replaceAll("[^\\w-]", "-").replaceAll("-+", "-"); 328 329 Matcher m = pattern.matcher(filteredName); 330 if (!m.matches()) 331 { 332 throw new IllegalArgumentException(filteredName + " doesn't match the expected regular expression : " + pattern.pattern()); 333 } 334 335 filteredName = filteredName.substring(m.end(1)); 336 337 // Remove characters '-' and '_' at the start and the end of the string 338 return StringUtils.strip(filteredName, "-_"); 339 } 340 341 /** 342 * Get all transitional page child from page name 343 * @param rootPage the root page 344 * @param pagePath the page path 345 * @return all transitional page child from page name 346 */ 347 public SortedSet<String> getTransitionalPagesName(Page rootPage, String pagePath) 348 { 349 String workspace = _workspaceSelector.getWorkspace(); 350 String site = rootPage.getSiteName(); 351 String contentType = getContentTypeId(rootPage); 352 String lang = rootPage.getSitemapName(); 353 354 PageCacheKey key = PageCacheKey.of(workspace, contentType, site, lang); 355 UDPagesCache udCache = getUDPagesCache().get(key, k -> _getUDPages(rootPage, workspace, contentType, lang)); 356 357 Map<String, SortedSet<String>> transitionalPages = udCache.transitionalPagesCache(); 358 String cachePagePath = getName(pagePath); 359 return transitionalPages.getOrDefault(cachePagePath, new TreeSet<>()); 360 } 361 362 /** 363 * Get all user page child from page name 364 * @param rootPage the root page 365 * @param pagePath the page path 366 * @return all user page child from page name 367 */ 368 public Map<String, String> getUserPagesContent(Page rootPage, String pagePath) 369 { 370 String workspace = _workspaceSelector.getWorkspace(); 371 String site = rootPage.getSiteName(); 372 String contentType = getContentTypeId(rootPage); 373 String lang = rootPage.getSitemapName(); 374 375 PageCacheKey key = PageCacheKey.of(workspace, contentType, site, lang); 376 UDPagesCache udCache = getUDPagesCache().get(key, k -> _getUDPages(rootPage, workspace, contentType, lang)); 377 378 Map<String, Map<String, String>> userPages = udCache.userPagesCache(); 379 String cachePagePath = getName(pagePath); 380 return userPages.getOrDefault(cachePagePath, new HashMap<>()); 381 } 382 383 /** 384 * Get the UD cache by page path 385 * For transitional pages returning a map as {'p' : [a, e], 'p/a' : [], 'p/e' : []} 386 * For user pages returning a map as {'p' : {userContent1: user://xxxxxxx1, userContent2: user://xxxxxxx2,}, 'p/a' : {userContent1: user://xxxxxxx1}, 'p/e' : {userContent2: user://xxxxxxx2}} 387 * @param rootPage the root page 388 * @param workspace the workspace 389 * @param contentType the content type 390 * @param lang the language 391 * @return the UD pages cache 392 */ 393 private UDPagesCache _getUDPages(Page rootPage, String workspace, String contentType, String lang) 394 { 395 // Getting all user content with its classification identifier defined in the root page 396 Map<Content, String> transformedValuesByContent = _getTransformedValuesByContent(rootPage); 397 398 // Computing transitional pages cache 399 Set<String> transformedValues = new HashSet<>(transformedValuesByContent.values()); 400 Map<String, SortedSet<String>> transitionalPagesCache = _getTransitionalPageByPagePath(transformedValues); 401 402 // Computing user pages cache 403 int depth = getDepth(rootPage); 404 Map<String, Map<String, String>> userPageCache = _getUserContentsByPagePath(transformedValuesByContent, depth); 405 406 getLogger().info("UD pages cache was built for workspace '{}' and content type '{}' and language '{}'", workspace, contentType, lang); 407 return new UDPagesCache(transitionalPagesCache, userPageCache); 408 } 409 410 private Map<String, SortedSet<String>> _getTransitionalPageByPagePath(Set<String> transformedValues) 411 { 412 Map<String, SortedSet<String>> transitionalPageByPath = new HashMap<>(); 413 for (String value : transformedValues) 414 { 415 char[] charArray = value.toCharArray(); 416 for (int i = 0; i < charArray.length; i++) 417 { 418 String lastChar = String.valueOf(charArray[i]); 419 if (i == 0) 420 { 421 // case _root 422 SortedSet<String> root = transitionalPageByPath.getOrDefault("_root", new TreeSet<>()); 423 if (!root.contains(lastChar)) 424 { 425 root.add(lastChar); 426 } 427 transitionalPageByPath.put("_root", root); 428 } 429 else 430 { 431 String currentPrefixWithoutLastChar = value.substring(0, i); // if value == "debu", equals to "deb" 432 String currentPathWithoutLastChar = StringUtils.join(currentPrefixWithoutLastChar.toCharArray(), '/'); // if value == "debu", equals to "d/e/b" 433 SortedSet<String> childPageNames = transitionalPageByPath.getOrDefault(currentPathWithoutLastChar, new TreeSet<>()); 434 if (!childPageNames.contains(lastChar)) 435 { 436 childPageNames.add(lastChar); // if value == "debu", add "u" in childPageNames for key "d/e/b" 437 } 438 transitionalPageByPath.put(currentPathWithoutLastChar, childPageNames); 439 } 440 } 441 } 442 443 return transitionalPageByPath; 444 } 445 446 private Map<String, Map<String, String>> _getUserContentsByPagePath(Map<Content, String> transformedValuesByContent, int depth) 447 { 448 Map<String, Map<String, String>> contentsByPath = new LinkedHashMap<>(); 449 if (depth == 0) 450 { 451 Map<String, String> rootContents = new LinkedHashMap<>(); 452 for (Content content : transformedValuesByContent.keySet()) 453 { 454 rootContents.put(content.getName(), content.getId()); 455 } 456 457 contentsByPath.put("_root", rootContents); 458 return contentsByPath; 459 } 460 461 for (Content content : transformedValuesByContent.keySet()) 462 { 463 String transformedValue = transformedValuesByContent.get(content); 464 for (int i = 0; i < depth; i++) 465 { 466 String currentPrefix = StringUtils.substring(transformedValue, 0, i + 1); 467 String currentPath = StringUtils.join(currentPrefix.toCharArray(), '/'); 468 Map<String, String> contentsForPath = contentsByPath.getOrDefault(currentPath, new LinkedHashMap<>()); 469 470 String contentName = content.getName(); 471 if (!contentsForPath.containsKey(contentName)) 472 { 473 contentsForPath.put(contentName, content.getId()); 474 } 475 contentsByPath.put(currentPath, contentsForPath); 476 } 477 } 478 return contentsByPath; 479 } 480 481 /** 482 * Get all transformed values by content 483 * @param rootPage the root page 484 * @return the map of transformed values by content 485 */ 486 protected Map<Content, String> _getTransformedValuesByContent(Page rootPage) 487 { 488 // Get all contents which will appear in the sitemap 489 AmetysObjectIterable<Content> contents = getContentsForRootPage(rootPage); 490 491 // Get their classification attribute value 492 Map<Content, String> transformedValuesByContent = new LinkedHashMap<>(); 493 for (Content content : contents) 494 { 495 String value = getTransformedClassificationValue(rootPage, content); 496 if (value != null) 497 { 498 transformedValuesByContent.put(content, value); 499 } 500 } 501 return transformedValuesByContent; 502 } 503 504 /** 505 * Get the user contents for a given root page 506 * @param rootPage the root page 507 * @return the user contents 508 */ 509 public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage) 510 { 511 String contentType = getContentTypeId(rootPage); 512 String lang = rootPage.getSitemapName(); 513 514 Set<String> subTypes = _contentTypeEP.getSubTypes(contentType); 515 516 List<Expression> contentTypeExpressions = new ArrayList<>(); 517 contentTypeExpressions.add(new ContentTypeExpression(Operator.EQ, contentType)); 518 for (String subType : subTypes) 519 { 520 contentTypeExpressions.add(new ContentTypeExpression(Operator.EQ, subType)); 521 } 522 523 Expression contentTypeExpression = new OrExpression(contentTypeExpressions.toArray(new Expression[subTypes.size() + 1])); 524 525 Expression finalExpr = new AndExpression(contentTypeExpression, new LanguageExpression(Operator.EQ, lang)); 526 527 SortCriteria sort = new SortCriteria(); 528 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 529 530 String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort); 531 532 return _resolver.query(xPathQuery); 533 } 534 535 /** 536 * Gets name form path name 537 * @param pathName the path name 538 * @return the name 539 */ 540 public String getName(String pathName) 541 { 542 String prefix = "page-"; 543 String name = ""; 544 for (String transitionalPageName : pathName.split("/")) 545 { 546 if (!name.equals("")) 547 { 548 name += "/"; 549 } 550 name += Strings.CS.startsWith(transitionalPageName, prefix) ? StringUtils.substringAfter(transitionalPageName, prefix) : transitionalPageName; 551 } 552 return name; 553 } 554 555 /** 556 * Checks if name contains only Unicode digits and if so, prefix it with "page-" 557 * @param name The page name 558 * @return The potentially prefixed page name 559 */ 560 public String getPathName(String name) 561 { 562 return StringUtils.isNumeric(name) ? "page-" + name : name; 563 } 564 565 /** 566 * Clear root page cache 567 * @param rootPage the root page 568 */ 569 public void clearCache(Page rootPage) 570 { 571 clearCache(getContentTypeId(rootPage)); 572 } 573 574 /** 575 * Clear root page cache 576 * @param contentTypeId the content type id 577 */ 578 public void clearCache(String contentTypeId) 579 { 580 getUDPagesCache().invalidate(PageCacheKey.of(null, contentTypeId, null, null)); 581 582 getRootPagesCache().invalidateAll(); 583 } 584 585 /** 586 * Cache of the user directory root pages. 587 * The cache store a Set of TODO indexed by the workspaceName, siteName, siteMapName 588 * @return the cache 589 */ 590 protected Cache<RootPageCacheKey, Set<String>> getRootPagesCache() 591 { 592 return _abstractCacheManager.get(ROOT_PAGES_CACHE); 593 } 594 595 /** 596 * Key to index a user directory root page in a cache 597 */ 598 protected static final class RootPageCacheKey extends AbstractCacheKey 599 { 600 /** 601 * Basic constructor 602 * @param workspaceName the workspace name. Can be null. 603 * @param siteName the site name. Can be null. 604 * @param language the sitemap name. Can be null. 605 */ 606 public RootPageCacheKey(String workspaceName, String siteName, String language) 607 { 608 super(workspaceName, siteName, language); 609 } 610 611 /** 612 * Generate a cache key 613 * @param workspaceName the workspace name. Can be null. 614 * @param siteName the site name. Can be null. 615 * @param language the sitemap name. Can be null. 616 * @return the cache key 617 */ 618 public static RootPageCacheKey of(String workspaceName, String siteName, String language) 619 { 620 return new RootPageCacheKey(workspaceName, siteName, language); 621 } 622 } 623 624 /** 625 * Cache of the user directory user pages and transitional page. 626 * The cache store a {@link UDPagesCache} containing the transitional pages cache and the user pages cache. 627 * The cache is indexed by workspaceName, siteName, siteMapName, pageName. 628 * @return the cache 629 */ 630 protected Cache<PageCacheKey, UDPagesCache> getUDPagesCache() 631 { 632 return _abstractCacheManager.get(UD_PAGES_CACHE); 633 } 634 635 /** 636 * Key to index a user directory page in a cache 637 */ 638 protected static final class PageCacheKey extends AbstractCacheKey 639 { 640 /** 641 * Basic constructor 642 * @param workspaceName the workspace name. Can be null. 643 * @param contentTypeId the contentType id. Can be null. 644 * @param siteName the site name. Can be null. 645 * @param language the sitemap name. Can be null. 646 */ 647 public PageCacheKey(String workspaceName, String contentTypeId, String siteName, String language) 648 { 649 super(workspaceName, contentTypeId, siteName, language); 650 } 651 652 /** 653 * Generate a cache key 654 * @param workspaceName the workspace name. Can be null. 655 * @param contentTypeId the contentType id. Can be null. 656 * @param siteName the site name. Can be null. 657 * @param language the sitemap name. Can be null. 658 * @return the cache key 659 */ 660 public static PageCacheKey of(String workspaceName, String contentTypeId, String siteName, String language) 661 { 662 return new PageCacheKey(workspaceName, contentTypeId, siteName, language); 663 } 664 } 665 666 /** 667 * User directory pages cache 668 * @param transitionalPagesCache the cache for transitional pages. The cache store a {@link Map} of (content path, sorted set of transitional page path). 669 * @param userPagesCache the cache for user pages. The cache store a {@link Map} of (content path, (content name, content id)) of all the content of the page. 670 */ 671 protected record UDPagesCache(Map<String, SortedSet<String>> transitionalPagesCache, Map<String, Map<String, String>> userPagesCache) { /** */ } 672}