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