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