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