001/* 002 * Copyright 2012 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.odfweb.repository; 017 018import java.util.Arrays; 019import java.util.Collections; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.Value; 030 031import org.apache.avalon.framework.activity.Initializable; 032import org.apache.avalon.framework.component.Component; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.commons.lang3.StringUtils; 037import org.apache.commons.lang3.Strings; 038 039import org.ametys.cms.content.ContentHelper; 040import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 041import org.ametys.cms.contenttype.ContentTypesHelper; 042import org.ametys.cms.repository.Content; 043import org.ametys.core.cache.AbstractCacheManager; 044import org.ametys.core.cache.Cache; 045import org.ametys.core.ui.Callable; 046import org.ametys.core.util.I18nUtils; 047import org.ametys.core.util.URIUtils; 048import org.ametys.odf.ProgramItem; 049import org.ametys.odf.catalog.CatalogsManager; 050import org.ametys.odf.course.Course; 051import org.ametys.odf.enumeration.OdfReferenceTableHelper; 052import org.ametys.odf.orgunit.RootOrgUnitProvider; 053import org.ametys.odf.program.AbstractProgram; 054import org.ametys.odf.program.Program; 055import org.ametys.odf.program.SubProgram; 056import org.ametys.odf.tree.OdfClassificationHandler; 057import org.ametys.odf.tree.OdfClassificationHandler.LevelValue; 058import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 059import org.ametys.plugins.odfweb.restrictions.OdfProgramRestriction; 060import org.ametys.plugins.odfweb.restrictions.OdfProgramRestrictionManager; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.plugins.repository.AmetysRepositoryException; 064import org.ametys.plugins.repository.jcr.JCRAmetysObject; 065import org.ametys.plugins.repository.jcr.NameHelper; 066import org.ametys.plugins.repository.provider.WorkspaceSelector; 067import org.ametys.plugins.repository.query.expression.Expression; 068import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression; 069import org.ametys.runtime.i18n.I18nizableText; 070import org.ametys.runtime.model.ModelItem; 071import org.ametys.runtime.plugin.component.AbstractLogEnabled; 072import org.ametys.web.repository.page.Page; 073import org.ametys.web.repository.page.PageQueryHelper; 074import org.ametys.web.repository.site.Site; 075import org.ametys.web.repository.sitemap.Sitemap; 076 077import com.google.common.collect.ImmutableList; 078 079/** 080 * Component providing methods to retrieve ODF virtual pages, such as the ODF root, 081 * level 1 and 2 metadata names, and so on. 082 */ 083public class OdfPageHandler extends AbstractLogEnabled implements Component, Initializable, Serviceable 084{ 085 /** The avalon role. */ 086 public static final String ROLE = OdfPageHandler.class.getName(); 087 088 /** First level attribute name. */ 089 public static final String LEVEL1_ATTRIBUTE_NAME = "firstLevel"; 090 091 /** Second level attribute name. */ 092 public static final String LEVEL2_ATTRIBUTE_NAME = "secondLevel"; 093 094 /** Catalog data name. */ 095 public static final String CATALOG_DATA_NAME = "odf-root-catalog"; 096 097 /** Content types that are not eligible for first and second level */ 098 // See ODF-1115 Exclude the mentions enumerator from the list : 099 protected static final List<String> NON_ELIGIBLE_CTYPES_FOR_LEVEL = Arrays.asList("org.ametys.plugins.odf.Content.programItem", "odf-enumeration.Mention"); 100 101 private static final String __ODF_ROOT_PAGES_CACHE = OdfPageHandler.class.getName() + "$odfRootPages"; 102 private static final String __HAS_ODF_ROOT_CACHE = OdfPageHandler.class.getName() + "$hasOdfRootPage"; 103 private static final String __PROGRAM_LEVEL_PATH_CACHE = OdfPageHandler.class.getName() + "$programLevelPath"; 104 private static final String __PROGRAM_RESTRICTION_CACHE = OdfPageHandler.class.getName() + "$programRestriction"; 105 106 private static final String __ROOT_CACHE_ALL_SITES_KEY = "ALL"; 107 private static final String __ROOT_CACHE_ALL_SITEMAPS_KEY = "ALL"; 108 109 /** The ametys object resolver. */ 110 protected AmetysObjectResolver _resolver; 111 112 /** The i18n utils. */ 113 protected I18nUtils _i18nUtils; 114 115 /** The content type extension point. */ 116 protected ContentTypeExtensionPoint _cTypeEP; 117 118 /** The ODF Catalog enumeration */ 119 protected CatalogsManager _catalogsManager; 120 121 /** The workspace selector. */ 122 protected WorkspaceSelector _workspaceSelector; 123 124 /** Avalon service manager */ 125 protected ServiceManager _manager; 126 127 /** Restriction manager */ 128 protected OdfProgramRestrictionManager _odfRestrictionsManager; 129 130 /** Content types helper */ 131 protected ContentTypesHelper _contentTypesHelper; 132 133 /** Content helper */ 134 protected ContentHelper _contentHelper; 135 136 /** Odf reference table helper */ 137 protected OdfReferenceTableHelper _odfReferenceTableHelper; 138 139 /** Root orgunit provider */ 140 protected RootOrgUnitProvider _orgUnitProvider; 141 142 /** Root orgunit provider */ 143 protected OdfClassificationHandler _odfClassificationHandler; 144 145 /** The cache manager */ 146 protected AbstractCacheManager _cacheManager; 147 148 @Override 149 public void service(ServiceManager serviceManager) throws ServiceException 150 { 151 _manager = serviceManager; 152 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 153 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 154 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 155 _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE); 156 _catalogsManager = (CatalogsManager) serviceManager.lookup(CatalogsManager.ROLE); 157 _odfRestrictionsManager = (OdfProgramRestrictionManager) serviceManager.lookup(OdfProgramRestrictionManager.ROLE); 158 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 159 _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE); 160 _odfReferenceTableHelper = (OdfReferenceTableHelper) serviceManager.lookup(OdfReferenceTableHelper.ROLE); 161 _orgUnitProvider = (RootOrgUnitProvider) serviceManager.lookup(RootOrgUnitProvider.ROLE); 162 _odfClassificationHandler = (OdfClassificationHandler) serviceManager.lookup(OdfClassificationHandler.ROLE); 163 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 164 } 165 166 @Override 167 public void initialize() throws Exception 168 { 169 _cacheManager.createMemoryCache(__ODF_ROOT_PAGES_CACHE, 170 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_LABEL"), 171 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_DESCRIPTION"), 172 true, 173 null); 174 175 _cacheManager.createMemoryCache(__HAS_ODF_ROOT_CACHE, 176 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_LABEL"), 177 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_DESCRIPTION"), 178 true, 179 null); 180 181 _cacheManager.createRequestCache(__PROGRAM_LEVEL_PATH_CACHE, 182 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_LABEL"), 183 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_DESCRIPTION"), 184 false); 185 186 _cacheManager.createRequestCache(__PROGRAM_RESTRICTION_CACHE, 187 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_LABEL"), 188 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_DESCRIPTION"), 189 false); 190 } 191 192 /** 193 * Get the first ODF root page. 194 * @param siteName The site name 195 * @param sitemapName The sitemap's name 196 * @return a ODF root page or null if not found. 197 * @throws AmetysRepositoryException if an error occurs 198 */ 199 public Page getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException 200 { 201 Set<Page> rootPages = getOdfRootPages(siteName, sitemapName); 202 return rootPages.isEmpty() ? null : rootPages.iterator().next(); 203 } 204 205 /** 206 * Get ODF root page of a specific catalog. 207 * @param siteName The site name 208 * @param sitemapName The sitemap name 209 * @param catalogName The catalog name 210 * @return the ODF root page or null if not found. 211 * @throws AmetysRepositoryException if an error occurs 212 */ 213 public Page getOdfRootPage(String siteName, String sitemapName, String catalogName) throws AmetysRepositoryException 214 { 215 String catalogToCompare = catalogName != null ? catalogName : ""; 216 217 for (Page odfRootPage : getOdfRootPages(siteName, sitemapName)) 218 { 219 if (catalogToCompare.equals(getCatalog(odfRootPage))) 220 { 221 return odfRootPage; 222 } 223 } 224 225 return null; 226 } 227 228 /** 229 * Get the id of ODF root pages 230 * @param siteName The site name 231 * @param sitemapName The sitemap name 232 * @return The ids of ODF root pages 233 * @throws AmetysRepositoryException if an error occurs. 234 */ 235 @Callable(rights = Callable.NO_CHECK_REQUIRED) // only retrieve page ids 236 public List<String> getOdfRootPageIds(String siteName, String sitemapName) throws AmetysRepositoryException 237 { 238 Set<Page> pages = getOdfRootPages(siteName, sitemapName); 239 return pages.stream().map(p -> p.getId()).collect(Collectors.toList()); 240 } 241 242 /** 243 * Get the ODF root pages. 244 * @param siteName the current site. 245 * @param sitemapName the current sitemap/language. 246 * @return the ODF root pages 247 * @throws AmetysRepositoryException if an error occurs. 248 */ 249 public Set<Page> getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 250 { 251 Cache<OdfRootPageCacheKey, Set<String>> cache = _getOdfRootPagesCache(); 252 253 String workspaceName = _workspaceSelector.getWorkspace(); 254 255 Set<String> rootPageIds = cache.get(OdfRootPageCacheKey.of(workspaceName, Objects.toString(siteName, __ROOT_CACHE_ALL_SITES_KEY), Objects.toString(sitemapName, __ROOT_CACHE_ALL_SITEMAPS_KEY)), item -> { 256 return _getOdfRootPages(siteName, sitemapName) 257 .stream() 258 .map(Page::getId) 259 .collect(Collectors.toSet()); 260 }); 261 262 return rootPageIds.stream() 263 .map(id -> (Page) _resolver.resolveById(id)) 264 .collect(Collectors.toSet()); 265 } 266 267 /** 268 * Test if the given site has at least one sitemap with an odf root page. 269 * @param site the site to test. 270 * @return true if the site has at least one sitemap with an odf root page, false otherwise. 271 */ 272 public boolean hasOdfRootPage(Site site) 273 { 274 Cache<HasOdfRootPageCacheKey, Boolean> cache = _getHasOdfRootPageCache(); 275 276 String workspace = _workspaceSelector.getWorkspace(); 277 278 return cache.get(HasOdfRootPageCacheKey.of(workspace, site.getName()), item -> { 279 280 Iterator<Sitemap> sitemaps = site.getSitemaps().iterator(); 281 while (sitemaps.hasNext()) 282 { 283 String sitemapName = sitemaps.next().getName(); 284 285 if (!getOdfRootPages(site.getName(), sitemapName).isEmpty()) 286 { 287 return true; 288 } 289 } 290 291 return false; 292 }); 293 } 294 295 /** 296 * Determines if the program is part of the site restrictions 297 * @param rootPage The ODF root page 298 * @param program The program 299 * @return <code>true</code> the program is part of the site restrictions 300 */ 301 public boolean isValidRestriction(Page rootPage, Program program) 302 { 303 Cache<ProgramInRootCacheKey, Boolean> cache = _cacheManager.get(__PROGRAM_RESTRICTION_CACHE); 304 305 return cache.get(ProgramInRootCacheKey.of(rootPage.getId(), program.getId()), isValid -> { 306 // Check catalog 307 if (!program.getCatalog().equals(getCatalog(rootPage))) 308 { 309 return false; 310 } 311 312 // Check language 313 if (!program.getLanguage().equals(rootPage.getSitemapName())) 314 { 315 return false; 316 } 317 318 // Check site restrictions 319 OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage); 320 if (restriction != null) 321 { 322 return restriction.contains(program); 323 } 324 325 return true; 326 }); 327 } 328 329 /** 330 * Clear the ODF root page cache. 331 */ 332 public void clearRootCache() 333 { 334 _getOdfRootPagesCache().invalidateAll(); 335 _getHasOdfRootPageCache().invalidateAll(); 336 } 337 338 /** 339 * Clear the ODF root page cache for a given site and language. 340 * @param siteName the current site. 341 * @param sitemapName the current sitemap/language. 342 */ 343 public void clearRootCache(String siteName, String sitemapName) 344 { 345 _getOdfRootPagesCache().invalidate(OdfRootPageCacheKey.of(null, siteName, sitemapName)); 346 _getHasOdfRootPageCache().invalidate(HasOdfRootPageCacheKey.of(null, siteName)); 347 } 348 349 /** 350 * Determines if the page is a ODF root page 351 * @param page The page to test 352 * @return true if the page is a ODF root page 353 */ 354 public boolean isODFRootPage (Page page) 355 { 356 if (page instanceof JCRAmetysObject) 357 { 358 try 359 { 360 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 361 Node node = jcrPage.getNode(); 362 363 if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 364 { 365 Value[] values = node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues(); 366 367 boolean hasValue = false; 368 for (int i = 0; i < values.length && !hasValue; i++) 369 { 370 hasValue = FirstLevelPageFactory.class.getName().equals(values[i].getString()); 371 } 372 373 return hasValue; 374 } 375 else 376 { 377 return false; 378 } 379 } 380 catch (RepositoryException e) 381 { 382 return false; 383 } 384 } 385 386 return false; 387 388 } 389 390 /** 391 * Get the ODF root page. 392 * @param siteName the current site. 393 * @param sitemapName the current sitemap/language. 394 * @return the ODF root page or null if not found. 395 * @throws AmetysRepositoryException if an error occurs. 396 */ 397 protected Page _getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException 398 { 399 Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName()); 400 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 401 402 AmetysObjectIterable<Page> pages = _resolver.query(query); 403 Page page = pages.stream().findFirst().orElse(null); 404 405 return page; 406 } 407 408 /** 409 * Get the ODF root page. 410 * @param siteName the current site. 411 * @param sitemapName the current sitemap/language. 412 * @return the ODF root page or null if not found. 413 * @throws AmetysRepositoryException if an error occurs. 414 */ 415 protected Set<Page> _getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 416 { 417 Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName()); 418 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 419 420 return _resolver.<Page>query(query).stream().collect(Collectors.toSet()); 421 } 422 423 /** 424 * Get the catalog value of the ODF root page 425 * @param rootPage The ODF root page 426 * @return the catalog value 427 */ 428 public String getCatalog (Page rootPage) 429 { 430 return rootPage.getValueOrDefault(CATALOG_DATA_NAME, StringUtils.EMPTY); 431 } 432 433 /** 434 * Get the first level metadata name. 435 * @param siteName the site name. 436 * @param sitemapName the sitemap name. 437 * @param catalog the current selected catalog. 438 * @return the first level metadata name. 439 */ 440 public String getLevel1Metadata(String siteName, String sitemapName, String catalog) 441 { 442 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 443 444 return getLevel1Metadata(rootPage); 445 } 446 447 /** 448 * Get the first level metadata name. 449 * @param rootPage the ODF root page. 450 * @return the first level metadata name. 451 */ 452 public String getLevel1Metadata(Page rootPage) 453 { 454 return rootPage.getValue(LEVEL1_ATTRIBUTE_NAME); 455 } 456 457 /** 458 * Get the second level metadata name. 459 * @param siteName the site name. 460 * @param sitemapName the sitemap name. 461 * @param catalog the current selected catalog. 462 * @return the second level metadata name. 463 */ 464 public String getLevel2Metadata(String siteName, String sitemapName, String catalog) 465 { 466 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 467 468 return getLevel2Metadata(rootPage); 469 } 470 471 /** 472 * Get the second level metadata name. 473 * @param rootPage the ODF root page. 474 * @return the second level metadata name. 475 */ 476 public String getLevel2Metadata(Page rootPage) 477 { 478 return rootPage.getValue(LEVEL2_ATTRIBUTE_NAME); 479 } 480 481 /** 482 * Get the first level metadata values (with translated label). 483 * @param siteName the site name. 484 * @param sitemapName the sitemap name. 485 * @param catalog the current selected catalog. 486 * @return the first level metadata values. 487 */ 488 public Map<String, LevelValue> getLevel1Values(String siteName, String sitemapName, String catalog) 489 { 490 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 491 492 return getLevel1Values(rootPage); 493 } 494 495 /** 496 * Get the level value of a program by extracting and transforming the raw program value at the desired metadata path 497 * @param program The program 498 * @param levelMetaPath The desired metadata path that represent a level 499 * @return The final level value 500 */ 501 public String getProgramLevelValue(Program program, String levelMetaPath) 502 { 503 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, levelMetaPath); 504 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 505 } 506 507 /** 508 * Get the first level value of a program by extracting and transforming the raw program value 509 * @param rootPage The root page 510 * @param program The program 511 * @return The final level value or <code>null</code> if not found 512 */ 513 public String getProgramLevel1Value(Page rootPage, Program program) 514 { 515 String level1Metadata = getLevel1Metadata(rootPage); 516 if (StringUtils.isNotBlank(level1Metadata)) 517 { 518 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level1Metadata); 519 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 520 } 521 else 522 { 523 return null; 524 } 525 } 526 527 /** 528 * Get the second level value of a program by extracting and transforming the raw program value 529 * @param rootPage The root page 530 * @param program The program 531 * @return The final level value or <code>null</code> if not found 532 */ 533 public String getProgramLevel2Value(Page rootPage, Program program) 534 { 535 String level2Metadata = getLevel2Metadata(rootPage); 536 if (StringUtils.isNotBlank(level2Metadata)) 537 { 538 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level2Metadata); 539 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 540 } 541 else 542 { 543 return null; 544 } 545 } 546 547 /** 548 * Get the orgunit identifier given an uai code 549 * @param rootPage Odf root page 550 * @param uaiCode The uai code 551 * @return The orgunit id or null if not found 552 */ 553 public String getOrgunitIdFromUaiCode(Page rootPage, String uaiCode) 554 { 555 return _odfClassificationHandler.getOrgunitIdFromUaiCode(rootPage.getSitemapName(), uaiCode); 556 } 557 558 /** 559 * Get the programs available for a ODF root page, taking account the site's restrictions 560 * @param rootPage The ODF root page 561 * @param level1 filters results with a level1 value. Can be null. 562 * @param level2 filters results with a level2 value. Can be null. 563 * @param programCode expected program code. Can be null. 564 * @param programName expected program name. Can be null. 565 * @return an iterator over resulting programs 566 */ 567 public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName) 568 { 569 return getProgramsWithRestrictions(rootPage, getLevel1Metadata(rootPage), level1, getLevel2Metadata(rootPage), level2, programCode, programName); 570 } 571 572 /** 573 * Get the programs available for a ODF root page, taking account the site's restrictions 574 * @param rootPage The ODF root page 575 * @param level1Metadata metadata name for first level 576 * @param level1 filters results with a level1 value. Can be null. 577 * @param level2Metadata metadata name for second level 578 * @param level2 filters results with a level2 value. Can be null. 579 * @param programCode expected program code. Can be null. 580 * @param programName expected program name. Can be null. 581 * @return an iterator over resulting programs 582 */ 583 public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1Metadata, String level1, String level2Metadata, String level2, String programCode, String programName) 584 { 585 OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage); 586 return _odfClassificationHandler.getPrograms(getCatalog(rootPage), rootPage.getSitemapName(), level1Metadata, level1, level2Metadata, level2, programCode, programName, restriction == null ? null : ImmutableList.of(restriction.getExpression())); 587 } 588 589 /** 590 * Get the first level metadata values (with translated label) 591 * @param rootPage the ODF root page. 592 * @return the first level metadata values. Can be empty if there is no level1 attribute on root page. 593 */ 594 public Map<String, LevelValue> getLevel1Values(Page rootPage) 595 { 596 String level1Value = getLevel1Metadata(rootPage); 597 if (StringUtils.isNotBlank(level1Value)) 598 { 599 return _odfClassificationHandler.getLevelValues(level1Value, rootPage.getSitemapName()); 600 } 601 else 602 { 603 return Collections.EMPTY_MAP; 604 } 605 } 606 607 /** 608 * Get the second level metadata values (with translated label). 609 * @param siteName the site name. 610 * @param sitemapName the sitemap name. 611 * @param catalog the current selected catalog. 612 * @return the second level metadata values. 613 */ 614 public Map<String, LevelValue> getLevel2Values(String siteName, String sitemapName, String catalog) 615 { 616 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 617 618 return getLevel2Values(rootPage); 619 } 620 621 /** 622 * Get the second level metadata values (with translated label). 623 * @param rootPage the ODF root page. 624 * @return the second level metadata values. Can be empty if there is no level2 attribute on root page. 625 */ 626 public Map<String, LevelValue> getLevel2Values(Page rootPage) 627 { 628 String level2Value = getLevel2Metadata(rootPage); 629 if (StringUtils.isNotBlank(level2Value)) 630 { 631 return _odfClassificationHandler.getLevelValues(level2Value, rootPage.getSitemapName()); 632 } 633 else 634 { 635 return Collections.EMPTY_MAP; 636 } 637 } 638 639 /** 640 * Encode level value to be use into a URI. 641 * @param value The raw value 642 * @return the encoded value 643 */ 644 public String encodeLevelValue(String value) 645 { 646 String encodedValue = Strings.CS.replace(value, "-", "@2D"); 647 encodedValue = Strings.CS.replace(encodedValue, "/", "@2F"); 648 encodedValue = Strings.CS.replace(encodedValue, ":", "@3A"); 649 encodedValue = Strings.CS.replace(encodedValue, "?", "@3F"); 650 return URIUtils.encodePathSegment(encodedValue); 651 } 652 653 /** 654 * Decode level value used in a URI 655 * @param value The encoded value 656 * @return the decoded value 657 */ 658 public String decodeLevelValue(String value) 659 { 660 String decodedValue = URIUtils.decode(value); 661 decodedValue = Strings.CS.replace(decodedValue, "@3F", "?"); 662 decodedValue = Strings.CS.replace(decodedValue, "@3A", ":"); 663 decodedValue = Strings.CS.replace(decodedValue, "@2F", "/"); 664 return Strings.CS.replace(decodedValue, "@2D", "-"); 665 } 666 667 /** 668 * Returns the page's name of a {@link ProgramItem}. 669 * Only {@link AbstractProgram} and {@link Course} can have a page. 670 * @param item The program item 671 * @return The page's name 672 * @throws IllegalArgumentException if the program item is not a {@link AbstractProgram} nor a {@link Course}. 673 */ 674 public String getPageName (ProgramItem item) 675 { 676 if (item instanceof AbstractProgram || item instanceof Course) 677 { 678 String filteredTitle = ""; 679 try 680 { 681 filteredTitle = NameHelper.filterName(((Content) item).getTitle()); 682 } 683 catch (IllegalArgumentException e) 684 { 685 // title does not match the expected regular expression : ^([0-9-_]*)[a-z].*$, use default title 686 if (item instanceof Program) 687 { 688 filteredTitle = "program"; 689 } 690 else if (item instanceof SubProgram) 691 { 692 filteredTitle = "subprogram"; 693 } 694 else if (item instanceof Course) 695 { 696 filteredTitle = "course"; 697 } 698 } 699 700 return filteredTitle + "-" + item.getCode(); 701 } 702 else 703 { 704 throw new IllegalArgumentException("Illegal program item : no page can be associated for a program item of type " + item.getClass().getName()); 705 } 706 } 707 708 /** 709 * Get the eligible enumerated attribute definitions for ODF page level 710 * @return the eligible attribute definitions 711 */ 712 public Map<String, ModelItem> getEligibleAttributesForLevel() 713 { 714 return _odfClassificationHandler.getEligibleAttributesForLevel(); 715 } 716 717 /** 718 * Get the ODF catalogs 719 * @return the ODF catalogs 720 */ 721 public Map<String, I18nizableText> getCatalogs() 722 { 723 return _odfClassificationHandler.getCatalogs(); 724 } 725 726 /** 727 * Get the enumerated attribute definitions for the given content type. 728 * Attribute with enumerator or content attribute are considered as enumerated 729 * @param programContentTypeId The content type's id 730 * @param allowMultiple <code>true</code> true to allow multiple attributes 731 * @return The definitions of enumerated attributes 732 */ 733 public Map<String, ModelItem> getEnumeratedAttributes(String programContentTypeId, boolean allowMultiple) 734 { 735 return _odfClassificationHandler.getEnumeratedAttributes(programContentTypeId, allowMultiple); 736 } 737 738 /** 739 * Compute the path from the root odf page, representing the first and second level pages. 740 * @param rootPage The odf root page 741 * @param parentProgram The program to compute 742 * @return the path, can be empty if no levels defined, and null if the parent program does not have values for levels attributes 743 */ 744 public String computeLevelsPath(Page rootPage, Program parentProgram) 745 { 746 Cache<ProgramInRootCacheKey, String> levelCache = _cacheManager.get(__PROGRAM_LEVEL_PATH_CACHE); 747 748 return levelCache.get(ProgramInRootCacheKey.of(rootPage.getId(), parentProgram.getId()), item -> { 749 // Level 1 is defined => Check the value 750 if (getLevel1Metadata(rootPage) != null) 751 { 752 String level1 = getProgramLevel1Value(rootPage, parentProgram); 753 754 // Value for level 1 is defined => Check for the second level 755 if (StringUtils.isNotBlank(level1)) 756 { 757 // Level 2 is defined => Check the value 758 if (getLevel2Metadata(rootPage) != null) 759 { 760 String level2 = getProgramLevel2Value(rootPage, parentProgram); 761 762 // Value for level 2 is defined => Return the level 2 page 763 if (StringUtils.isNotBlank(level2)) 764 { 765 Page secondLevelPage = findSecondLevelPage(rootPage, level1, level2); 766 return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName(); 767 } 768 769 // Value for level 2 is not defined => Return null 770 return null; 771 } 772 773 // Level 2 is not defined => Return the level 1 page 774 return findFirstLevelPage(rootPage, level1).getName(); 775 } 776 777 // Value for level 1 is not defined => Return null 778 return null; 779 } 780 781 // Level 1 is not defined => Return an empty path, all pages are on the root page 782 return StringUtils.EMPTY; 783 }); 784 } 785 786 /** 787 * Build the level 1 identifier 788 * @param rootPage The ODF root page 789 * @param level1Value The level1 name 790 * @return the identifier beginning by odfLevel1://... 791 */ 792 public String buildLevel1Id(Page rootPage, String level1Value) 793 { 794 // E.g: odfLevel1://XA?rootId=... 795 return "odfLevel1://" + encodeLevelValue(level1Value) + "?rootId=" + rootPage.getId(); 796 } 797 798 /** 799 * Build the level 2 identifier 800 * @param rootPage The ODF root page 801 * @param level1Value The level1 name 802 * @param level2Value The level2 name 803 * @return the identifier beginning by odfLevel2://... 804 */ 805 public String buildLevel2Id(Page rootPage, String level1Value, String level2Value) 806 { 807 // E.g: odfLevel2://XA/ALL?rootId=... 808 return "odfLevel2://" + encodeLevelValue(level1Value) + "/" + encodeLevelValue(level2Value) + "?rootId=" + rootPage.getId(); 809 } 810 811 /** 812 * Get the first level page from the given root page with the level 1 value. 813 * @param rootPage The odf root page 814 * @param level1Value The first level value 815 * @return a first level page 816 */ 817 public FirstLevelPage findFirstLevelPage(Page rootPage, String level1Value) 818 { 819 // Calculate the real path 820 return _resolver.resolveById(buildLevel1Id(rootPage, level1Value)); 821 } 822 823 /** 824 * Get the second level page from the given root page with the level 1 and level 2 values. 825 * @param rootPage The odf root page 826 * @param level1Value The first level value 827 * @param level2Value The second level value 828 * @return a second level page 829 */ 830 public SecondLevelPage findSecondLevelPage(Page rootPage, String level1Value, String level2Value) 831 { 832 // Calculate the real path 833 return _resolver.resolveById(buildLevel2Id(rootPage, level1Value, level2Value)); 834 } 835 836 /** 837 * Add an intermediate redirect page if the called page name doesn't match the real page name. 838 * @param page The page 839 * @param name The called page name 840 * @return The page maybe included in a {@link RedirectPage} 841 */ 842 public Page addRedirectIfNeeded(Page page, String name) 843 { 844 // Decode both because in the case of page.getName(), only the code is encoded (it is 845 // normal) and the name is already partially decoded before, so we encode both to be 846 // sure to compare the same values. 847 if (!decodeLevelValue(name).equals(decodeLevelValue(page.getName()))) 848 { 849 getLogger().warn("Redirect path '{}' to '{}' page", name, page.getName()); 850 return new RedirectPage(page instanceof RedirectPage redirectPage ? redirectPage.getRedirectPage() : page); 851 } 852 return page; 853 } 854 855 /** 856 * Explore the queue path if it is not empty 857 * @param page The root page to explore 858 * @param queuePath The path 859 * @return The child page, or given page if queue path is empty 860 */ 861 public Page exploreQueuePath(Page page, String queuePath) 862 { 863 if (StringUtils.isNotEmpty(queuePath)) 864 { 865 return page.getChild(queuePath); 866 } 867 return page; 868 } 869 870 private Cache<OdfRootPageCacheKey, Set<String>> _getOdfRootPagesCache() 871 { 872 return _cacheManager.get(__ODF_ROOT_PAGES_CACHE); 873 } 874 875 private Cache<HasOdfRootPageCacheKey, Boolean> _getHasOdfRootPageCache() 876 { 877 return _cacheManager.get(__HAS_ODF_ROOT_CACHE); 878 } 879 880 static class OdfRootPageCacheKey extends AbstractCacheKey 881 { 882 public OdfRootPageCacheKey(String workspaceName, String siteName, String sitemapName) 883 { 884 super(workspaceName, siteName, sitemapName); 885 } 886 887 public static OdfRootPageCacheKey of(String workspaceName, String siteName, String sitemapName) 888 { 889 return new OdfRootPageCacheKey(workspaceName, siteName, sitemapName); 890 } 891 } 892 893 static class HasOdfRootPageCacheKey extends AbstractCacheKey 894 { 895 public HasOdfRootPageCacheKey(String workspaceName, String siteName) 896 { 897 super(workspaceName, siteName); 898 } 899 900 public static HasOdfRootPageCacheKey of(String workspaceName, String siteName) 901 { 902 return new HasOdfRootPageCacheKey(workspaceName, siteName); 903 } 904 } 905 906 static class ProgramInRootCacheKey extends AbstractCacheKey 907 { 908 public ProgramInRootCacheKey(String rootPageId, String programId) 909 { 910 super(rootPageId, programId); 911 } 912 913 public static ProgramInRootCacheKey of(String rootPageId, String programId) 914 { 915 return new ProgramInRootCacheKey(rootPageId, programId); 916 } 917 } 918}