001/* 002 * Copyright 2017 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.odf.tree; 017 018import java.text.Normalizer; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.Set; 030import java.util.TreeMap; 031import java.util.stream.Collectors; 032import java.util.stream.Stream; 033 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.avalon.framework.service.Serviceable; 039import org.apache.commons.lang3.StringUtils; 040import org.xml.sax.SAXException; 041 042import org.ametys.cms.contenttype.ContentAttributeDefinition; 043import org.ametys.cms.contenttype.ContentType; 044import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 045import org.ametys.cms.contenttype.ContentTypesHelper; 046import org.ametys.cms.data.ContentDataHelper; 047import org.ametys.cms.data.type.ModelItemTypeConstants; 048import org.ametys.cms.repository.Content; 049import org.ametys.cms.repository.ContentQueryHelper; 050import org.ametys.cms.repository.ContentTypeExpression; 051import org.ametys.cms.repository.LanguageExpression; 052import org.ametys.core.cache.AbstractCacheManager; 053import org.ametys.core.cache.Cache; 054import org.ametys.core.util.I18nUtils; 055import org.ametys.core.util.LambdaUtils; 056import org.ametys.odf.ProgramItem; 057import org.ametys.odf.catalog.Catalog; 058import org.ametys.odf.catalog.CatalogsManager; 059import org.ametys.odf.enumeration.OdfReferenceTableEntry; 060import org.ametys.odf.enumeration.OdfReferenceTableHelper; 061import org.ametys.odf.orgunit.OrgUnit; 062import org.ametys.odf.orgunit.OrgUnitFactory; 063import org.ametys.odf.orgunit.RootOrgUnitProvider; 064import org.ametys.odf.program.Program; 065import org.ametys.odf.program.ProgramFactory; 066import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 067import org.ametys.plugins.repository.AmetysObject; 068import org.ametys.plugins.repository.AmetysObjectIterable; 069import org.ametys.plugins.repository.AmetysObjectResolver; 070import org.ametys.plugins.repository.UnknownAmetysObjectException; 071import org.ametys.plugins.repository.model.RepeaterDefinition; 072import org.ametys.plugins.repository.provider.WorkspaceSelector; 073import org.ametys.plugins.repository.query.QueryHelper; 074import org.ametys.plugins.repository.query.SortCriteria; 075import org.ametys.plugins.repository.query.expression.AndExpression; 076import org.ametys.plugins.repository.query.expression.Expression; 077import org.ametys.plugins.repository.query.expression.Expression.Operator; 078import org.ametys.plugins.repository.query.expression.StringExpression; 079import org.ametys.runtime.i18n.I18nizableText; 080import org.ametys.runtime.model.ElementDefinition; 081import org.ametys.runtime.model.Enumerator; 082import org.ametys.runtime.model.ModelItem; 083import org.ametys.runtime.model.ModelItemContainer; 084import org.ametys.runtime.plugin.component.AbstractLogEnabled; 085 086import com.google.common.collect.Maps; 087 088/** 089 * Component providing methods to retrieve ODF virtual pages, such as the ODF root, 090 * level 1 and 2 metadata names, and so on. 091 */ 092public class OdfClassificationHandler extends AbstractLogEnabled implements Component, Initializable, Serviceable 093{ 094 /** The avalon role. */ 095 public static final String ROLE = OdfClassificationHandler.class.getName(); 096 097 /** First level attribute name. */ 098 public static final String LEVEL1_ATTRIBUTE_NAME = "firstLevel"; 099 100 /** Second level attribute name. */ 101 public static final String LEVEL2_ATTRIBUTE_NAME = "secondLevel"; 102 103 /** Catalog attribute name. */ 104 public static final String CATALOG_ATTRIBUTE_NAME = "odf-root-catalog"; 105 106 /** Constant for the {@link Cache} id for the {@link LevelValue}s objects in cache by {@link LevelValuesCacheKey}. */ 107 public static final String LEVEL_VALUES_CACHE = OdfClassificationHandler.class.getName() + "$LevelValues"; 108 109 /** The default level 1 attribute. */ 110 protected static final String DEFAULT_LEVEL1_ATTRIBUTE = "degree"; 111 112 /** The default level 2 attribute. */ 113 protected static final String DEFAULT_LEVEL2_ATTRIBUTE = "domain"; 114 115 /** Content types that are not eligible for first and second level */ 116 // See ODF-1115 Exclude the mentions enumerator from the list : 117 protected static final List<String> NON_ELIGIBLE_CTYPES_FOR_LEVEL = Arrays.asList("org.ametys.plugins.odf.Content.programItem", "odf-enumeration.Mention"); 118 119 /** The ametys object resolver. */ 120 protected AmetysObjectResolver _resolver; 121 122 /** The i18n utils. */ 123 protected I18nUtils _i18nUtils; 124 125 /** The content type extension point. */ 126 protected ContentTypeExtensionPoint _cTypeEP; 127 128 /** The ODF Catalog enumeration */ 129 protected CatalogsManager _catalogsManager; 130 131 /** The workspace selector. */ 132 protected WorkspaceSelector _workspaceSelector; 133 134 /** Avalon service manager */ 135 protected ServiceManager _manager; 136 137 /** Content types helper */ 138 protected ContentTypesHelper _contentTypesHelper; 139 140 /** Odf reference table helper */ 141 protected OdfReferenceTableHelper _odfReferenceTableHelper; 142 143 /** Root orgunit provider */ 144 protected RootOrgUnitProvider _orgUnitProvider; 145 146 /** The cache manager */ 147 protected AbstractCacheManager _cacheManager; 148 149 @Override 150 public void service(ServiceManager serviceManager) throws ServiceException 151 { 152 _manager = serviceManager; 153 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 154 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 155 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 156 _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE); 157 _catalogsManager = (CatalogsManager) serviceManager.lookup(CatalogsManager.ROLE); 158 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 159 _odfReferenceTableHelper = (OdfReferenceTableHelper) serviceManager.lookup(OdfReferenceTableHelper.ROLE); 160 _orgUnitProvider = (RootOrgUnitProvider) serviceManager.lookup(RootOrgUnitProvider.ROLE); 161 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 162 } 163 164 @Override 165 public void initialize() throws Exception 166 { 167 _cacheManager.createMemoryCache(LEVEL_VALUES_CACHE, 168 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_LEVEL_VALUES_LABEL"), 169 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_LEVEL_VALUES_DESC"), 170 true, 171 null); 172 } 173 174 /** 175 * Get the ODF catalogs 176 * @return the ODF catalogs 177 */ 178 public Map<String, I18nizableText> getCatalogs () 179 { 180 Map<String, I18nizableText> catalogs = new HashMap<>(); 181 182 for (Catalog catalog : _catalogsManager.getCatalogs()) 183 { 184 catalogs.put(catalog.getName(), new I18nizableText(catalog.getTitle())); 185 } 186 187 return catalogs; 188 } 189 190 /** 191 * True if the program attribute is eligible 192 * @param attributePath the attribute path 193 * @param allowMultiple true is we allow multiple attribute 194 * @return true if the program attribute is eligible 195 */ 196 public boolean isEligibleMetadataForLevel(String attributePath, boolean allowMultiple) 197 { 198 ContentType cType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 199 200 if (cType.hasModelItem(attributePath)) 201 { 202 ModelItem modelItem = cType.getModelItem(attributePath); 203 return _isModelItemEligible(modelItem, allowMultiple); 204 } 205 else 206 { 207 return false; 208 } 209 } 210 211 /** 212 * Get the eligible enumerated attributes for ODF page level 213 * @return the eligible attributes 214 */ 215 public Map<String, ModelItem> getEligibleAttributesForLevel() 216 { 217 return getEnumeratedAttributes(ProgramFactory.PROGRAM_CONTENT_TYPE, false); 218 } 219 220 /** 221 * Get the enumerated attribute definitions for the given content type. 222 * Attribute with enumerator or content attribute are considered as enumerated 223 * @param programContentTypeId The content type's id 224 * @param allowMultiple <code>true</code> true to allow multiple attribute 225 * @return The definitions of enumerated attributes 226 */ 227 public Map<String, ModelItem> getEnumeratedAttributes(String programContentTypeId, boolean allowMultiple) 228 { 229 ContentType cType = _cTypeEP.getExtension(programContentTypeId); 230 231 return _collectEligibleChildAttribute(cType, allowMultiple) 232 .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue)); 233 } 234 235 private Stream<Map.Entry<String, ModelItem>> _collectEligibleChildAttribute(ModelItemContainer modelItemContainer, boolean allowMultiple) 236 { 237 // repeaters are not supported 238 if (modelItemContainer instanceof RepeaterDefinition) 239 { 240 return Stream.empty(); 241 } 242 243 return modelItemContainer.getModelItems().stream() 244 .flatMap(modelItem -> 245 { 246 if (_isModelItemEligible(modelItem, allowMultiple)) 247 { 248 return Stream.of(Maps.immutableEntry(modelItem.getPath(), modelItem)); 249 } 250 else if (modelItem instanceof ModelItemContainer) 251 { 252 return _collectEligibleChildAttribute((ModelItemContainer) modelItem, allowMultiple); 253 } 254 else 255 { 256 return Stream.empty(); 257 } 258 }); 259 } 260 261 @SuppressWarnings("static-access") 262 private boolean _isModelItemEligible(ModelItem modelItem, boolean allowMultiple) 263 { 264 if (!(modelItem instanceof ElementDefinition)) 265 { 266 return false; 267 } 268 269 ElementDefinition elementDefinition = (ElementDefinition) modelItem; 270 if (elementDefinition.isMultiple() && !allowMultiple) 271 { 272 return false; 273 } 274 275 276 String typeId = elementDefinition.getType().getId(); 277 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(typeId)) 278 { 279 String contentTypeId = ((ContentAttributeDefinition) elementDefinition).getContentTypeId(); 280 281 Stream<String> selfAndAncestors = Stream.concat( 282 Stream.of(contentTypeId), 283 _contentTypesHelper.getAncestors(contentTypeId).stream() 284 ); 285 286 return selfAndAncestors.noneMatch(NON_ELIGIBLE_CTYPES_FOR_LEVEL::contains); 287 } 288 else if (ModelItemTypeConstants.STRING_TYPE_ID.equals(typeId)) 289 { 290 return elementDefinition.getEnumerator() != null && !elementDefinition.getName().startsWith("dc_"); 291 } 292 else 293 { 294 return false; 295 } 296 } 297 298 /** 299 * Get the level value of a program by extracting and transforming the raw program value at the desired metadata path 300 * @param program The program 301 * @param levelMetaPath The desired metadata path that represent a level 302 * @return The list of final level values 303 */ 304 public List<String> getProgramLevelValues(Program program, String levelMetaPath) 305 { 306 List<String> rawValues = getProgramLevelRawValues(program, levelMetaPath); 307 return rawValues.stream() 308 .map(e -> _convertRawValue2LevelValue(levelMetaPath, e)) 309 .filter(Objects::nonNull) 310 .collect(Collectors.toList()); 311 } 312 313 /** 314 * Get the level value of a program by extracting and transforming the raw program value at the desired attribute path 315 * @param program The program 316 * @param levelAttributePath The desired attribute path that represent a level 317 * @return The list of level raw value 318 */ 319 public List<String> getProgramLevelRawValues(Program program, String levelAttributePath) 320 { 321 String typeId = program.getType(levelAttributePath).getId(); 322 323 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(typeId)) 324 { 325 if (program.isMultiple(levelAttributePath)) 326 { 327 return ContentDataHelper.getContentIdsListFromMultipleContentData(program, levelAttributePath); 328 } 329 else 330 { 331 return Collections.singletonList(ContentDataHelper.getContentIdFromContentData(program, levelAttributePath)); 332 } 333 } 334 else if (org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(typeId)) 335 { 336 if (program.isMultiple(levelAttributePath)) 337 { 338 return Arrays.asList(program.getValue(levelAttributePath)); 339 } 340 else 341 { 342 return Collections.singletonList(program.getValue(levelAttributePath)); 343 } 344 } 345 else 346 { 347 throw new IllegalArgumentException("The attribute at path '" + levelAttributePath + "' is not an eligible attribute for level"); 348 } 349 } 350 351 /** 352 * Convert the attribute raw value into a level value 353 * @param attributePath The path of the attribute corresponding to the level 354 * @param rawLevelValue The raw level value 355 * @return the converted value or <code>null</code> if there is no level value for this raw value 356 */ 357 protected String _convertRawValue2LevelValue(String attributePath, String rawLevelValue) 358 { 359 // FIXME a raw <=> level value cache would be useful, but need a specific cache management strategy 360 361 String levelValue = rawLevelValue; 362 363 ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 364 ModelItem modelItem = programCType.getModelItem(attributePath); 365 366 String attributeContentTypeId = null; 367 if (modelItem instanceof ContentAttributeDefinition) 368 { 369 attributeContentTypeId = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 370 } 371 372 if (StringUtils.isNotEmpty(attributeContentTypeId)) 373 { 374 // Odf reference table 375 if (_odfReferenceTableHelper.isTableReference(attributeContentTypeId)) 376 { 377 levelValue = _convertRaw2LevelForRefTable(rawLevelValue); 378 } 379 // Orgunit 380 else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(attributeContentTypeId)) 381 { 382 levelValue = _convertRaw2LevelForOrgUnit(rawLevelValue); 383 } 384 // Other content 385 else 386 { 387 levelValue = _convertRaw2LevelForContent(rawLevelValue); 388 } 389 } 390 391 return StringUtils.defaultIfEmpty(levelValue, null); 392 } 393 394 private String _convertRaw2LevelForRefTable(String contentId) 395 { 396 return _odfReferenceTableHelper.getItemCode(contentId); 397 } 398 399 private String _convertRaw2LevelForOrgUnit(String orgUnitId) 400 { 401 try 402 { 403 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 404 return orgUnit.getUAICode(); 405 } 406 catch (UnknownAmetysObjectException e) 407 { 408 getLogger().warn("Unable to get level value for orgunit with id '{}'.", orgUnitId, e); 409 return ""; 410 } 411 } 412 413 private String _convertRaw2LevelForContent(String contentId) 414 { 415 try 416 { 417// Content content = _resolver.resolveById(contentId); 418 // FIXME name might not be unique between sites, languages, content without site etc... 419 // return content.getName(); 420 return contentId; 421 } 422 catch (UnknownAmetysObjectException e) 423 { 424 getLogger().warn("Unable to get level value for content with id '{}'.", contentId, e); 425 return ""; 426 } 427 } 428 429 private String _convertLevel2RawForRefTable(String metaContentType, String levelValue) 430 { 431 return Optional.ofNullable(_odfReferenceTableHelper.getItemFromCode(metaContentType, levelValue)) 432 .map(OdfReferenceTableEntry::getId) 433 .orElse(null); 434 } 435 436 private String _convertLevel2RawForContent(String levelValue) 437 { 438 // must return the content id 439 // FIXME currently the level value is the content id (see #_convertRaw2LevelForContent) 440 return levelValue; 441 } 442 443 /** 444 * Clear the cache of available values for levels used for ODF virtual pages 445 */ 446 public void clearLevelValues() 447 { 448 _getLevelValuesCache().invalidateAll(); 449 } 450 451 /** 452 * Clear the cache of available values for level 453 * @param lang the language. Can be null to clear values for all languages 454 * @param metadataPath the path of level's metadata 455 */ 456 public void clearLevelValues(String metadataPath, String lang) 457 { 458 Cache<LevelValuesCacheKey, Map<String, LevelValue>> levelValuesCache = _getLevelValuesCache(); 459 460 LevelValuesCacheKey levelValuesKey = LevelValuesCacheKey.of(metadataPath, lang, null); 461 levelValuesCache.invalidate(levelValuesKey); 462 } 463 464 /** 465 * Get the first level metadata values (with translated label). 466 * @param metadata Metadata of first level 467 * @param language Lang to get 468 * @return the first level metadata values. 469 */ 470 public Map<String, LevelValue> getLevelValues(String metadata, String language) 471 { 472 Cache<LevelValuesCacheKey, Map<String, LevelValue>> levelValuesCache = _getLevelValuesCache(); 473 474 String workspace = _workspaceSelector.getWorkspace(); 475 LevelValuesCacheKey levelValuesKey = LevelValuesCacheKey.of(metadata, language, workspace); 476 477 return levelValuesCache.get(levelValuesKey, __ -> _getLevelValues(metadata, language)); 478 } 479 480 /** 481 * Encode level value to be use into a URI. 482 * Double-encode characters ':', '-' and '/'. 483 * @param value The raw value 484 * @return the encoded value 485 */ 486 public String encodeLevelValue(String value) 487 { 488 String encodedValue = StringUtils.replace(value, "-", "@2D"); 489 encodedValue = StringUtils.replace(encodedValue, "/", "@2F"); 490 encodedValue = StringUtils.replace(encodedValue, ":", "@3A"); 491 return encodedValue; 492 } 493 494 /** 495 * Decode level value used in a URI 496 * @param value The encoded value 497 * @return the decoded value 498 */ 499 public String decodeLevelValue(String value) 500 { 501 String decodedValue = StringUtils.replace(value, "@2F", "/"); 502 decodedValue = StringUtils.replace(decodedValue, "@3A", ":"); 503 return StringUtils.replace(decodedValue, "@2D", "-"); 504 } 505 506 /** 507 * Get the available values of a program's attribute to be used as a level in the virtual ODF page hierarchy. 508 * @param attributePath the attribute path. 509 * @param language the language. 510 * @return the available attribute values. 511 */ 512 private Map<String, LevelValue> _getLevelValues(String attributePath, String language) 513 { 514 try 515 { 516 ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 517 ModelItem modelItem = programCType.getModelItem(attributePath); 518 519 String attributeContentTypeId = null; 520 if (modelItem instanceof ContentAttributeDefinition) 521 { 522 attributeContentTypeId = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 523 } 524 525 if (StringUtils.isNotEmpty(attributeContentTypeId)) 526 { 527 // Odf reference table 528 if (_odfReferenceTableHelper.isTableReference(attributeContentTypeId)) 529 { 530 return _getLevelValuesForRefTable(attributeContentTypeId, language); 531 } 532 // Orgunit 533 else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(attributeContentTypeId)) 534 { 535 return _getLevelValuesForOrgUnits(); 536 } 537 // Other content 538 else 539 { 540 return _getLevelValuesForContentType(attributeContentTypeId, language); 541 } 542 } 543 544 Enumerator<?> enumerator = null; 545 if (modelItem instanceof ElementDefinition) 546 { 547 enumerator = ((ElementDefinition) modelItem).getEnumerator(); 548 } 549 550 if (enumerator != null) 551 { 552 return _getLevelValuesForEnumerator(language, enumerator); 553 } 554 } 555 catch (Exception e) 556 { 557 // Log and return empty map. 558 getLogger().error("Error retrieving values for metadata {} in language {}", attributePath, language, e); 559 } 560 561 return Maps.newHashMap(); 562 } 563 564 private Map<String, LevelValue> _getLevelValuesForRefTable(String metaContentType, String language) 565 { 566 Map<String, LevelValue> levelValues = new LinkedHashMap<>(); 567 568 List<OdfReferenceTableEntry> entries = _odfReferenceTableHelper.getItems(metaContentType); 569 for (OdfReferenceTableEntry entry : entries) 570 { 571 if (StringUtils.isEmpty(entry.getCode())) 572 { 573 getLogger().warn("There is no code for entry {} ({}) of reference table '{}'. It will be ignored for classification", entry.getLabel(language), entry.getId(), metaContentType); 574 } 575 else if (levelValues.containsKey(entry.getCode())) 576 { 577 getLogger().warn("Duplicate key code {} into reference table '{}'. The entry {} ({}) will be ignored for classification", entry.getCode(), metaContentType, entry.getLabel(language), entry.getId()); 578 } 579 else 580 { 581 LevelValue levelValue = new LevelValue( 582 entry.getLabel(language), 583 entry.getOrder() 584 ); 585 levelValues.put(entry.getCode(), levelValue); 586 } 587 } 588 589 return levelValues; 590 } 591 592 private Map<String, LevelValue> _getLevelValuesForOrgUnits() 593 { 594 String rootOrgUnitId = _orgUnitProvider.getRootId(); 595 Set<String> childOrgUnitIds = _orgUnitProvider.getChildOrgUnitIds(rootOrgUnitId, true); 596 597 Map<String, LevelValue> levelValues = new LinkedHashMap<>(); 598 599 for (String childOUId : childOrgUnitIds) 600 { 601 OrgUnit childOU = _resolver.resolveById(childOUId); 602 if (StringUtils.isEmpty(childOU.getUAICode())) 603 { 604 getLogger().warn("There is no UAI code for orgunit {} ({}). It will be ignored for classification", childOU.getTitle(), childOU.getId()); 605 } 606 else if (levelValues.containsKey(childOU.getUAICode())) 607 { 608 getLogger().warn("Duplicate UAI code {}. The orgunit {} ({}) will be ignored for classification", childOU.getUAICode(), childOU.getTitle(), childOU.getId()); 609 } 610 else 611 { 612 levelValues.put(childOU.getUAICode(), _convertToLevelValue(childOU.getTitle())); 613 } 614 } 615 return levelValues; 616 } 617 618 private Map<String, LevelValue> _getLevelValuesForContentType(String metaContentType, String language) 619 { 620 Expression expr = new AndExpression( 621 new ContentTypeExpression(Operator.EQ, metaContentType), 622 new LanguageExpression(Operator.EQ, language) 623 ); 624 625 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 626 627 return _resolver.<Content>query(xpathQuery).stream() 628 .collect(LambdaUtils.Collectors.toLinkedHashMap(Content::getId, c -> _convertToLevelValue(c.getTitle()))); 629 } 630 631 private <T extends Object> Map<String, LevelValue> _getLevelValuesForEnumerator(String language, Enumerator<T> enumerator) throws Exception 632 { 633 return enumerator.getTypedEntries().entrySet().stream() 634 .filter(entry -> StringUtils.isNotEmpty(entry.getKey().toString())) 635 .map(entry -> 636 { 637 String code = entry.getKey().toString(); 638 639 I18nizableText label = entry.getValue(); 640 String itemLabel = _i18nUtils.translate(label, language); 641 642 return Maps.immutableEntry(code, itemLabel); 643 }) 644 .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, e -> _convertToLevelValue(e.getValue()))); 645 } 646 647 /** 648 * Get a collection of programs corresponding to following parameters. 649 * @param catalog Name of the catalog. Can be null to get all programs matching other arguments. 650 * @param lang the content language. Can not be null. 651 * @param level1MetaPath Having a non-empty value for the metadata path 652 * @param level1 If this parameter is not null or empty and level1MetaPath too, we filter programs by the metadata level1MetaPath value of level1 653 * @param level2MetaPath Having a non-empty value for the metadata path 654 * @param level2 If this parameter is not null or empty and level2MetaPath too, we filter programs by the metadata level2MetaPath value of level2 655 * @param programCode The program's code. Can be null to get all programs matching other arguments. 656 * @param programName The program's name. Can be null to get all programs matching other arguments. 657 * @param additionalExpressions Additional expressions to add to search 658 * @return A collection of programs 659 */ 660 public AmetysObjectIterable<Program> getPrograms(String catalog, String lang, String level1MetaPath, String level1, String level2MetaPath, String level2, String programCode, String programName, Collection<Expression> additionalExpressions) 661 { 662 List<Expression> exprs = new ArrayList<>(); 663 664 exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 665 exprs.add(new LanguageExpression(Operator.EQ, lang)); 666 667 /* Level 1 */ 668 if (StringUtils.isNotEmpty(level1)) 669 { 670 exprs.add(new StringExpression(level1MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level1MetaPath, level1))); 671 } 672 else if (StringUtils.isNotBlank(level1MetaPath)) 673 { 674 exprs.add(new StringExpression(level1MetaPath, Operator.NE, "")); 675 } 676 677 /* Level 2 */ 678 if (StringUtils.isNotEmpty(level2)) 679 { 680 exprs.add(new StringExpression(level2MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level2MetaPath, level2))); 681 } 682 else if (StringUtils.isNotBlank(level2MetaPath)) 683 { 684 exprs.add(new StringExpression(level2MetaPath, Operator.NE, "")); 685 } 686 687 if (catalog != null) 688 { 689 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 690 } 691 692 if (StringUtils.isNotEmpty(programCode)) 693 { 694 exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, programCode)); 695 } 696 697 if (additionalExpressions != null) 698 { 699 exprs.addAll(additionalExpressions); 700 } 701 702 SortCriteria sortCriteria = new SortCriteria(); 703 sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 704 705 Expression contentExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 706 707 String xPathQuery = QueryHelper.getXPathQuery(StringUtils.defaultIfEmpty(programName, null), "ametys:content", contentExpression, sortCriteria); 708 return _resolver.query(xPathQuery); 709 } 710 711 /** 712 * Get the orgunit identifier given an uai code 713 * @param lang Language 714 * @param uaiCode The uai code 715 * @return The orgunit id or null if not found 716 */ 717 public String getOrgunitIdFromUaiCode(String lang, String uaiCode) 718 { 719 Expression ouExpression = new AndExpression( 720 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 721 new LanguageExpression(Operator.EQ, lang), 722 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 723 ); 724 725 String query = ContentQueryHelper.getContentXPathQuery(ouExpression); 726 return _resolver.query(query).stream().findFirst().map(AmetysObject::getId).orElse(null); 727 } 728 729 /** 730 * Convert a level value to the raw value 731 * @param lang The language 732 * @param levelAttribute The name of attribute holding the level 733 * @param levelValue The level value 734 * @return The raw value 735 */ 736 private String _convertLevelValue2RawValue(String lang, String levelAttribute, String levelValue) 737 { 738 // FIXME a raw <=> level value cache would be useful, but need a specific cache management strategy 739 740 String rawValue = null; 741 742 ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 743 ModelItem modelItem = programCType.getModelItem(levelAttribute); 744 745 String attributeContentTypeId = null; 746 if (modelItem instanceof ContentAttributeDefinition) 747 { 748 attributeContentTypeId = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 749 } 750 751 if (StringUtils.isNotEmpty(attributeContentTypeId)) 752 { 753 if (_odfReferenceTableHelper.isTableReference(attributeContentTypeId)) 754 { 755 rawValue = _convertLevel2RawForRefTable(attributeContentTypeId, levelValue); 756 } 757 // Orgunit 758 else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(attributeContentTypeId)) 759 { 760 rawValue = _convertLevel2RawForOrgUnit(lang, levelValue); 761 } 762 // Other content 763 else 764 { 765 rawValue = _convertLevel2RawForContent(levelValue); 766 } 767 } 768 769 return StringUtils.defaultIfEmpty(rawValue, levelValue); 770 } 771 772 private String _convertLevel2RawForOrgUnit(String lang, String levelValue) 773 { 774 return getOrgunitIdFromUaiCode(lang, levelValue); 775 } 776 777 /** 778 * Organize passed programs by levels into a Map. 779 * @param programs Programs to organize 780 * @param level1 Name of the metadata of first level 781 * @param level2 Name of the metadata of second level 782 * @return A Map of Map with a Collection of programs which representing the organization of programs by levels. 783 * @throws SAXException if an error occured 784 */ 785 public Map<String, Map<String, Collection<Program>>> organizeProgramsByLevels(AmetysObjectIterable<Program> programs, String level1, String level2) throws SAXException 786 { 787 Map<String, Map<String, Collection<Program>>> level1Map = new TreeMap<>(); 788 789 for (Program program : programs) 790 { 791 List<String> programL1RawValues = getProgramLevelRawValues(program, level1); 792 List<String> programL2RawValues = getProgramLevelRawValues(program, level2); 793 for (String programL1Value : programL1RawValues) 794 { 795 if (StringUtils.isNotEmpty(programL1Value)) 796 { 797 Map<String, Collection<Program>> level2Map = level1Map.computeIfAbsent(programL1Value, x -> new TreeMap<>()); 798 for (String programL2Value : programL2RawValues) 799 { 800 if (StringUtils.isNotEmpty(programL2Value)) 801 { 802 Collection<Program> programCache = level2Map.computeIfAbsent(programL2Value, x -> new ArrayList<>()); 803 programCache.add(program); 804 } 805 } 806 } 807 } 808 } 809 810 return level1Map; 811 } 812 813 private LevelValue _convertToLevelValue(String value) 814 { 815 return new LevelValue(value, Long.MAX_VALUE); 816 } 817 818 /** 819 * Wrapper object for a level value 820 */ 821 public static class LevelValue 822 { 823 private String _value; 824 private Long _order; 825 826 /** 827 * The constructor 828 * @param value the value 829 * @param order the order 830 */ 831 public LevelValue(String value, Long order) 832 { 833 _value = value; 834 _order = order; 835 } 836 837 /** 838 * Get the value 839 * @return the value 840 */ 841 public String getValue() 842 { 843 return _value; 844 } 845 846 /** 847 * Get the order 848 * @return the order 849 */ 850 public Long getOrder() 851 { 852 return _order; 853 } 854 855 /** 856 * Compare to a level value depends of the order first then the value 857 * @param levelValue the level value to compare 858 * @return the int value of the comparaison 859 */ 860 public int compareTo(LevelValue levelValue) 861 { 862 if (_order.equals(levelValue.getOrder())) 863 { 864 String value1 = Normalizer.normalize(_value, Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 865 String value2 = Normalizer.normalize(levelValue.getValue(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 866 867 return value1.compareToIgnoreCase(value2); 868 } 869 else 870 { 871 return _order.compareTo(levelValue.getOrder()); 872 } 873 } 874 } 875 876 private Cache<LevelValuesCacheKey, Map<String, LevelValue>> _getLevelValuesCache() 877 { 878 return _cacheManager.get(LEVEL_VALUES_CACHE); 879 } 880 881 private static final class LevelValuesCacheKey extends AbstractCacheKey 882 { 883 private LevelValuesCacheKey(String attributeName, String lang, String workspace) 884 { 885 super(attributeName, lang, workspace); 886 } 887 888 static LevelValuesCacheKey of(String attributeName, String lang, String workspace) 889 { 890 return new LevelValuesCacheKey(attributeName, lang, workspace); 891 } 892 } 893}