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.ContentQueryHelper;
051import org.ametys.cms.repository.ContentTypeExpression;
052import org.ametys.cms.repository.DefaultContent;
053import org.ametys.cms.repository.LanguageExpression;
054import org.ametys.core.util.I18nUtils;
055import org.ametys.core.util.LambdaUtils;
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                LambdaUtils.Collectors.toLinkedHashMap(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(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            getLogger().warn("Unable to get level value for orgunit with id '{}'.", orgUnitId, e);
355            return "";
356        }
357    }
358    
359    private String _convertRaw2LevelForContent(String contentId)
360    {
361        try
362        {
363//            Content content = _resolver.resolveById(contentId);
364            // FIXME name might not be unique between sites, languages, content without site etc...
365            // return content.getName();
366            return contentId;
367        }
368        catch (UnknownAmetysObjectException e)
369        {
370            getLogger().warn("Unable to get level value for content with id '{}'.", contentId, e);
371            return "";
372        }
373    }
374    
375    private String _convertLevel2RawForRefTable(String metaContentType, String levelValue)
376    {
377        return Optional.ofNullable(_odfReferenceTableHelper.getItemFromCode(metaContentType, levelValue))
378                .map(OdfReferenceTableEntry::getId)
379                .orElse(null);
380    }
381    
382    private String _convertLevel2RawForContent(String levelValue)
383    {
384        // must return the content id
385        // FIXME currently the level value is the content id (see #_convertRaw2LevelForContent)
386        return levelValue;
387    }
388    
389    /**
390     * Clear the cache of available values for levels used for ODF virtual pages
391     */
392    public void clearLevelValues()
393    {
394        _levelValuesCache.clear();
395    }
396    
397    /**
398     * Clear the cache  of available values for level 
399     * @param lang the language
400     * @param metadataPath the path of level's metadata
401     */
402    public void clearLevelValues(String metadataPath, String lang)
403    {
404        String cacheKey = metadataPath + "/" + lang;
405        if (_levelValuesCache.containsKey(cacheKey))
406        {
407            _levelValuesCache.remove(cacheKey);
408        }
409    }
410
411    /**
412     * Get the first level metadata values (with translated label).
413     * @param metadata Metadata of first level
414     * @param language Lang to get
415     * @return the first level metadata values.
416     */
417    public Map<String, String> getLevelValues(String metadata, String language)
418    {
419        Map<String, String> values;
420        
421        String cacheKey = metadata + "/" + language;
422        
423        if (_levelValuesCache.containsKey(cacheKey))
424        {
425            values = _levelValuesCache.get(cacheKey);
426        }
427        else
428        {
429            values = _getLevelValues(metadata, language);
430            _levelValuesCache.put(cacheKey, values);
431        }
432        
433        return values;
434    }
435
436    /**
437     * Encode level value to be use into a URI.
438     * Double-encode characters ':', '-' and '/'.
439     * @param value The raw value
440     * @return the encoded value
441     */
442    public String encodeLevelValue(String value)
443    {
444        String encodedValue = StringUtils.replace(value, "-", "@2D");
445        encodedValue = StringUtils.replace(encodedValue, "/", "@2F");
446        encodedValue = StringUtils.replace(encodedValue, ":", "@3A");
447        return encodedValue;
448    }
449    
450    /**
451     * Decode level value used in a URI
452     * @param value The encoded value
453     * @return the decoded value
454     */
455    public String decodeLevelValue(String value)
456    {
457        String decodedValue = StringUtils.replace(value, "@2F", "/");
458        decodedValue = StringUtils.replace(decodedValue, "@3A", ":");
459        return StringUtils.replace(decodedValue, "@2D", "-");
460    }
461    
462    /**
463     * Get the available values of a program's metadata to be used as a level in the virtual odf page hierarchy.
464     * @param metadata the metadata path.
465     * @param language the language.
466     * @return the available metadata values.
467     */
468    private Map<String, String> _getLevelValues(String metadata, String language)
469    {
470        try
471        {
472            ContentType programCType = _cTypeEP.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE);
473            MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(metadata, programCType);
474            
475            String metaContentType = metadataDefinition.getContentType();
476            
477            if (StringUtils.isNotEmpty(metaContentType))
478            {
479                // Odf reference table
480                if (_odfReferenceTableHelper.isTableReference(metaContentType))
481                {
482                    return _getLevelValuesForRefTable(metaContentType, language);
483                }
484                // Orgunit
485                else if (OrgUnitFactory.ORGUNIT_CONTENT_TYPE.equals(metaContentType))
486                {
487                    return _getLevelValuesForOrgUnits();
488                }
489                // Other content
490                else
491                {
492                    return _getLevelValuesForContentType(metaContentType, language);
493                }
494            }
495            
496            Enumerator enumerator = metadataDefinition.getEnumerator();
497            if (enumerator != null)
498            {
499                return _getLevelValuesForEnumerator(language, enumerator);
500            }
501        }
502        catch (Exception e)
503        {
504            // Log and return empty map.
505            getLogger().error("Error retrieving values for metadata {} in language {}", metadata, language, e);
506        }
507        
508        return Maps.newHashMap();
509    }
510    
511    private Map<String, String> _getLevelValuesForRefTable(String metaContentType, String language)
512    {
513        Map<String, String> levelValues = new LinkedHashMap<>();
514        
515        List<OdfReferenceTableEntry> entries = _odfReferenceTableHelper.getItems(metaContentType);
516        for (OdfReferenceTableEntry entry : entries)
517        {
518            if (StringUtils.isEmpty(entry.getCode()))
519            {
520                getLogger().warn("There is no code for entry {} ({}) of reference table '{}'. It will be ignored for classification", entry.getLabel(language), entry.getId(), metaContentType);
521            }
522            else if (levelValues.containsKey(entry.getCode()))
523            {
524                getLogger().warn("Duplicate key code {} into reference table '{}'. The entry {} ({}) will be ignored for classification", entry.getCode(), metaContentType, entry.getLabel(language), entry.getId());
525            }
526            else
527            {
528                levelValues.put(entry.getCode(), entry.getLabel(language));
529            }
530        }
531        
532        return levelValues;
533    }
534    
535    private Map<String, String> _getLevelValuesForOrgUnits()
536    {
537        String rootOrgUnitId = _orgUnitProvider.getRootId();
538        Set<String> childOrgUnitIds = _orgUnitProvider.getChildOrgUnitIds(rootOrgUnitId, true);
539        
540        Map<String, String> levelValues = new LinkedHashMap<>();
541        
542        for (String childOUId : childOrgUnitIds)
543        {
544            OrgUnit childOU = _resolver.resolveById(childOUId);
545            if (StringUtils.isEmpty(childOU.getUAICode()))
546            {
547                getLogger().warn("There is no UAI code for orgunit {} ({}). It will be ignored for classification", childOU.getTitle(), childOU.getId());
548            }
549            else if (levelValues.containsKey(childOU.getUAICode()))
550            {
551                getLogger().warn("Duplicate UAI code {}. The orgunit {} ({}) will be ignored for classification", childOU.getUAICode(), childOU.getTitle(), childOU.getId());
552            }
553            else
554            {
555                levelValues.put(childOU.getUAICode(), childOU.getTitle());
556            }
557        }
558        return levelValues;
559    }
560    
561    private Map<String, String> _getLevelValuesForContentType(String metaContentType, String language)
562    {
563        Expression expr = new AndExpression(
564            new ContentTypeExpression(Operator.EQ, metaContentType),
565            new LanguageExpression(Operator.EQ, language)
566        );
567        
568        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr);
569        
570        return _resolver.<Content>query(xpathQuery).stream()
571            .collect(LambdaUtils.Collectors.toLinkedHashMap(Content::getId, Content::getTitle));
572    }
573    
574    private Map<String, String> _getLevelValuesForEnumerator(String language,  Enumerator enumerator) throws Exception
575    {
576        return enumerator.getEntries().entrySet().stream()
577            .filter(entry -> StringUtils.isNotEmpty(entry.getKey().toString()))
578            .map(entry ->
579            {
580                String code = entry.getKey().toString();
581                
582                I18nizableText label = entry.getValue();
583                String itemLabel = _i18nUtils.translate(label, language);
584                
585                return Maps.immutableEntry(code, itemLabel);
586            })
587            .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue));
588    }
589    
590    /**
591     * Get a collection of programs corresponding to following parameters.
592     * @param catalog Name of the catalog. Can be null to get all programs matching other arguments.
593     * @param lang the content language. Can not be null.
594     * @param level1MetaPath Having a non-empty value for the metadata path
595     * @param level1 If this parameter is not null or empty and level1MetaPath too, we filter programs by the metadata level1MetaPath value of level1
596     * @param level2MetaPath Having a non-empty value for the metadata path
597     * @param level2 If this parameter is not null or empty and level2MetaPath too, we filter programs by the metadata level2MetaPath value of level2
598     * @param programCode The program's code. Can be null to get all programs matching other arguments.
599     * @param programName The program's name. Can be null to get all programs matching other arguments.
600     * @param additionalExpressions Additional expressions to add to search
601     * @return A collection of programs
602     */
603    public AmetysObjectIterable<Program> getPrograms(String catalog, String lang, String level1MetaPath, String level1, String level2MetaPath, String level2, String programCode, String programName, Collection<Expression> additionalExpressions)
604    {
605        List<Expression> exprs = new ArrayList<>();
606
607        exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
608        exprs.add(new LanguageExpression(Operator.EQ, lang));
609
610        /* Level 1 */
611        if (StringUtils.isNotEmpty(level1))
612        {
613            exprs.add(new StringExpression(level1MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level1MetaPath, level1)));
614        }
615        else if (StringUtils.isNotBlank(level1MetaPath))
616        {
617            exprs.add(new StringExpression(level1MetaPath, Operator.NE, ""));
618        }
619
620        /* Level 2 */
621        if (StringUtils.isNotEmpty(level2))
622        {
623            exprs.add(new StringExpression(level2MetaPath, Operator.EQ, _convertLevelValue2RawValue(lang, level2MetaPath, level2)));
624        }
625        else if (StringUtils.isNotBlank(level2MetaPath))
626        {
627            exprs.add(new StringExpression(level2MetaPath, Operator.NE, ""));
628        }
629        
630        if (catalog != null)
631        {
632            exprs.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalog));
633        }
634        
635        if (StringUtils.isNotEmpty(programCode))
636        {
637            exprs.add(new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, programCode));
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 LanguageExpression(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}