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 * Get the available values of a program's attribute to be used as a level in the virtual ODF page hierarchy. 483 * @param attributePath the attribute path. 484 * @param language the language. 485 * @return the available attribute values. 486 */ 487 private Map<String, LevelValue> _getLevelValues(String attributePath, String language) 488 { 489 try 490 { 491 ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 492 ModelItem modelItem = programCType.getModelItem(attributePath); 493 494 String attributeContentTypeId = null; 495 if (modelItem instanceof ContentAttributeDefinition) 496 { 497 attributeContentTypeId = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 498 } 499 500 if (StringUtils.isNotEmpty(attributeContentTypeId)) 501 { 502 // Odf reference table 503 if (_odfReferenceTableHelper.isTableReference(attributeContentTypeId)) 504 { 505 return _getLevelValuesForRefTable(attributeContentTypeId, language); 506 } 507 // Orgunit 508 else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(attributeContentTypeId)) 509 { 510 return _getLevelValuesForOrgUnits(); 511 } 512 // Other content 513 else 514 { 515 return _getLevelValuesForContentType(attributeContentTypeId, language); 516 } 517 } 518 519 Enumerator<?> enumerator = null; 520 if (modelItem instanceof ElementDefinition) 521 { 522 enumerator = ((ElementDefinition) modelItem).getEnumerator(); 523 } 524 525 if (enumerator != null) 526 { 527 return _getLevelValuesForEnumerator(language, enumerator); 528 } 529 } 530 catch (Exception e) 531 { 532 // Log and return empty map. 533 getLogger().error("Error retrieving values for metadata {} in language {}", attributePath, language, e); 534 } 535 536 return Maps.newHashMap(); 537 } 538 539 private Map<String, LevelValue> _getLevelValuesForRefTable(String metaContentType, String language) 540 { 541 Map<String, LevelValue> levelValues = new LinkedHashMap<>(); 542 543 List<OdfReferenceTableEntry> entries = _odfReferenceTableHelper.getItems(metaContentType); 544 for (OdfReferenceTableEntry entry : entries) 545 { 546 if (StringUtils.isEmpty(entry.getCode())) 547 { 548 getLogger().warn("There is no code for entry {} ({}) of reference table '{}'. It will be ignored for classification", entry.getLabel(language), entry.getId(), metaContentType); 549 } 550 else if (levelValues.containsKey(entry.getCode())) 551 { 552 getLogger().warn("Duplicate key code {} into reference table '{}'. The entry {} ({}) will be ignored for classification", entry.getCode(), metaContentType, entry.getLabel(language), entry.getId()); 553 } 554 else 555 { 556 LevelValue levelValue = new LevelValue( 557 entry.getLabel(language), 558 entry.getOrder() 559 ); 560 levelValues.put(entry.getCode(), levelValue); 561 } 562 } 563 564 return levelValues; 565 } 566 567 private Map<String, LevelValue> _getLevelValuesForOrgUnits() 568 { 569 String rootOrgUnitId = _orgUnitProvider.getRootId(); 570 Set<String> childOrgUnitIds = _orgUnitProvider.getChildOrgUnitIds(rootOrgUnitId, true); 571 572 Map<String, LevelValue> levelValues = new LinkedHashMap<>(); 573 574 for (String childOUId : childOrgUnitIds) 575 { 576 OrgUnit childOU = _resolver.resolveById(childOUId); 577 if (StringUtils.isEmpty(childOU.getUAICode())) 578 { 579 getLogger().warn("There is no UAI code for orgunit {} ({}). It will be ignored for classification", childOU.getTitle(), childOU.getId()); 580 } 581 else if (levelValues.containsKey(childOU.getUAICode())) 582 { 583 getLogger().warn("Duplicate UAI code {}. The orgunit {} ({}) will be ignored for classification", childOU.getUAICode(), childOU.getTitle(), childOU.getId()); 584 } 585 else 586 { 587 levelValues.put(childOU.getUAICode(), _convertToLevelValue(childOU.getTitle())); 588 } 589 } 590 return levelValues; 591 } 592 593 private Map<String, LevelValue> _getLevelValuesForContentType(String metaContentType, String language) 594 { 595 Expression expr = new AndExpression( 596 new ContentTypeExpression(Operator.EQ, metaContentType), 597 new LanguageExpression(Operator.EQ, language) 598 ); 599 600 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 601 602 return _resolver.<Content>query(xpathQuery).stream() 603 .collect(LambdaUtils.Collectors.toLinkedHashMap(Content::getId, c -> _convertToLevelValue(c.getTitle()))); 604 } 605 606 private <T extends Object> Map<String, LevelValue> _getLevelValuesForEnumerator(String language, Enumerator<T> enumerator) throws Exception 607 { 608 return enumerator.getEntries().entrySet().stream() 609 .filter(entry -> StringUtils.isNotEmpty(entry.getKey().toString())) 610 .map(entry -> 611 { 612 String code = entry.getKey().toString(); 613 614 I18nizableText label = entry.getValue(); 615 String itemLabel = _i18nUtils.translate(label, language); 616 617 return Maps.immutableEntry(code, itemLabel); 618 }) 619 .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, e -> _convertToLevelValue(e.getValue()))); 620 } 621 622 /** 623 * Get a collection of programs corresponding to following parameters. 624 * @param catalog Name of the catalog. Can be null to get all programs matching other arguments. 625 * @param lang the content language. Can not be null. 626 * @param level1MetaPath Having a non-empty value for the metadata path 627 * @param level1 If this parameter is not null or empty and level1MetaPath too, we filter programs by the metadata level1MetaPath value of level1 628 * @param level2MetaPath Having a non-empty value for the metadata path 629 * @param level2 If this parameter is not null or empty and level2MetaPath too, we filter programs by the metadata level2MetaPath value of level2 630 * @param programCode The program's code. Can be null to get all programs matching other arguments. 631 * @param programName The program's name. Can be null to get all programs matching other arguments. 632 * @param additionalExpressions Additional expressions to add to search 633 * @return A collection of programs 634 */ 635 public AmetysObjectIterable<Program> getPrograms(String catalog, String lang, String level1MetaPath, String level1, String level2MetaPath, String level2, String programCode, String programName, Collection<Expression> additionalExpressions) 636 { 637 List<Expression> exprs = new ArrayList<>(); 638 639 exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 640 exprs.add(new LanguageExpression(Operator.EQ, lang)); 641 642 /* Level 1 */ 643 if (StringUtils.isNotEmpty(level1)) 644 { 645 exprs.add(new StringExpression(level1MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level1MetaPath, level1))); 646 } 647 else if (StringUtils.isNotBlank(level1MetaPath)) 648 { 649 exprs.add(new StringExpression(level1MetaPath, Operator.NE, "")); 650 } 651 652 /* Level 2 */ 653 if (StringUtils.isNotEmpty(level2)) 654 { 655 exprs.add(new StringExpression(level2MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level2MetaPath, level2))); 656 } 657 else if (StringUtils.isNotBlank(level2MetaPath)) 658 { 659 exprs.add(new StringExpression(level2MetaPath, Operator.NE, "")); 660 } 661 662 if (catalog != null) 663 { 664 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 665 } 666 667 if (StringUtils.isNotEmpty(programCode)) 668 { 669 exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, programCode)); 670 } 671 672 if (additionalExpressions != null) 673 { 674 exprs.addAll(additionalExpressions); 675 } 676 677 SortCriteria sortCriteria = new SortCriteria(); 678 sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 679 680 Expression contentExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 681 682 String xPathQuery = QueryHelper.getXPathQuery(StringUtils.defaultIfEmpty(programName, null), "ametys:content", contentExpression, sortCriteria); 683 return _resolver.query(xPathQuery); 684 } 685 686 /** 687 * Get the orgunit identifier given an uai code 688 * @param lang Language 689 * @param uaiCode The uai code 690 * @return The orgunit id or null if not found 691 */ 692 public String getOrgunitIdFromUaiCode(String lang, String uaiCode) 693 { 694 Expression ouExpression = new AndExpression( 695 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 696 new LanguageExpression(Operator.EQ, lang), 697 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 698 ); 699 700 String query = ContentQueryHelper.getContentXPathQuery(ouExpression); 701 return _resolver.query(query).stream().findFirst().map(AmetysObject::getId).orElse(null); 702 } 703 704 /** 705 * Convert a level value to the raw value 706 * @param lang The language 707 * @param levelAttribute The name of attribute holding the level 708 * @param levelValue The level value 709 * @return The raw value 710 */ 711 private String _convertLevelValue2RawValue(String lang, String levelAttribute, String levelValue) 712 { 713 // FIXME a raw <=> level value cache would be useful, but need a specific cache management strategy 714 715 String rawValue = null; 716 717 ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE); 718 ModelItem modelItem = programCType.getModelItem(levelAttribute); 719 720 String attributeContentTypeId = null; 721 if (modelItem instanceof ContentAttributeDefinition) 722 { 723 attributeContentTypeId = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 724 } 725 726 if (StringUtils.isNotEmpty(attributeContentTypeId)) 727 { 728 if (_odfReferenceTableHelper.isTableReference(attributeContentTypeId)) 729 { 730 rawValue = _convertLevel2RawForRefTable(attributeContentTypeId, levelValue); 731 } 732 // Orgunit 733 else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(attributeContentTypeId)) 734 { 735 rawValue = _convertLevel2RawForOrgUnit(lang, levelValue); 736 } 737 // Other content 738 else 739 { 740 rawValue = _convertLevel2RawForContent(levelValue); 741 } 742 } 743 744 return StringUtils.defaultIfEmpty(rawValue, levelValue); 745 } 746 747 private String _convertLevel2RawForOrgUnit(String lang, String levelValue) 748 { 749 return getOrgunitIdFromUaiCode(lang, levelValue); 750 } 751 752 /** 753 * Organize passed programs by levels into a Map. 754 * @param programs Programs to organize 755 * @param level1 Name of the metadata of first level 756 * @param level2 Name of the metadata of second level 757 * @return A Map of Map with a Collection of programs which representing the organization of programs by levels. 758 * @throws SAXException if an error occured 759 */ 760 public Map<String, Map<String, Collection<Program>>> organizeProgramsByLevels(AmetysObjectIterable<Program> programs, String level1, String level2) throws SAXException 761 { 762 Map<String, Map<String, Collection<Program>>> level1Map = new TreeMap<>(); 763 764 for (Program program : programs) 765 { 766 List<String> programL1RawValues = getProgramLevelRawValues(program, level1); 767 List<String> programL2RawValues = getProgramLevelRawValues(program, level2); 768 for (String programL1Value : programL1RawValues) 769 { 770 if (StringUtils.isNotEmpty(programL1Value)) 771 { 772 Map<String, Collection<Program>> level2Map = level1Map.computeIfAbsent(programL1Value, x -> new TreeMap<>()); 773 for (String programL2Value : programL2RawValues) 774 { 775 if (StringUtils.isNotEmpty(programL2Value)) 776 { 777 Collection<Program> programCache = level2Map.computeIfAbsent(programL2Value, x -> new ArrayList<>()); 778 programCache.add(program); 779 } 780 } 781 } 782 } 783 } 784 785 return level1Map; 786 } 787 788 private LevelValue _convertToLevelValue(String value) 789 { 790 return new LevelValue(value, Long.MAX_VALUE); 791 } 792 793 /** 794 * Wrapper object for a level value 795 */ 796 public static class LevelValue 797 { 798 private String _value; 799 private Long _order; 800 801 /** 802 * The constructor 803 * @param value the value 804 * @param order the order 805 */ 806 public LevelValue(String value, Long order) 807 { 808 _value = value; 809 _order = order; 810 } 811 812 /** 813 * Get the value 814 * @return the value 815 */ 816 public String getValue() 817 { 818 return _value; 819 } 820 821 /** 822 * Get the order 823 * @return the order 824 */ 825 public Long getOrder() 826 { 827 return _order; 828 } 829 830 /** 831 * Compare to a level value depends of the order first then the value 832 * @param levelValue the level value to compare 833 * @return the int value of the comparaison 834 */ 835 public int compareTo(LevelValue levelValue) 836 { 837 if (_order.equals(levelValue.getOrder())) 838 { 839 String value1 = Normalizer.normalize(_value, Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 840 String value2 = Normalizer.normalize(levelValue.getValue(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 841 842 return value1.compareToIgnoreCase(value2); 843 } 844 else 845 { 846 return _order.compareTo(levelValue.getOrder()); 847 } 848 } 849 } 850 851 private Cache<LevelValuesCacheKey, Map<String, LevelValue>> _getLevelValuesCache() 852 { 853 return _cacheManager.get(LEVEL_VALUES_CACHE); 854 } 855 856 private static final class LevelValuesCacheKey extends AbstractCacheKey 857 { 858 private LevelValuesCacheKey(String attributeName, String lang, String workspace) 859 { 860 super(attributeName, lang, workspace); 861 } 862 863 static LevelValuesCacheKey of(String attributeName, String lang, String workspace) 864 { 865 return new LevelValuesCacheKey(attributeName, lang, workspace); 866 } 867 } 868}