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