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