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