001/*
002 *  Copyright 2011 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.enumeration;
017
018import java.util.Arrays;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.XMLUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.xml.sax.ContentHandler;
037import org.xml.sax.SAXException;
038
039import org.ametys.cms.contenttype.ContentAttributeDefinition;
040import org.ametys.cms.contenttype.ContentType;
041import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
042import org.ametys.cms.contenttype.ContentTypesHelper;
043import org.ametys.cms.data.ContentValue;
044import org.ametys.cms.repository.Content;
045import org.ametys.cms.repository.ContentQueryHelper;
046import org.ametys.cms.repository.ContentTypeExpression;
047import org.ametys.core.ui.Callable;
048import org.ametys.odf.ODFHelper;
049import org.ametys.odf.ProgramItem;
050import org.ametys.odf.program.AbstractProgram;
051import org.ametys.odf.program.Program;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectIterator;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056import org.ametys.plugins.repository.model.RepeaterDefinition;
057import org.ametys.plugins.repository.query.SortCriteria;
058import org.ametys.plugins.repository.query.expression.AndExpression;
059import org.ametys.plugins.repository.query.expression.BooleanExpression;
060import org.ametys.plugins.repository.query.expression.Expression;
061import org.ametys.plugins.repository.query.expression.Expression.Operator;
062import org.ametys.plugins.repository.query.expression.NotExpression;
063import org.ametys.plugins.repository.query.expression.StringExpression;
064import org.ametys.runtime.config.Config;
065import org.ametys.runtime.model.ElementDefinition;
066import org.ametys.runtime.model.ModelItem;
067import org.ametys.runtime.model.ModelItemContainer;
068import org.ametys.runtime.model.View;
069import org.ametys.runtime.model.ViewElement;
070import org.ametys.runtime.model.ViewItem;
071import org.ametys.runtime.model.ViewItemContainer;
072import org.ametys.runtime.plugin.component.AbstractLogEnabled;
073
074/**
075 * This component handles ODF reference tables
076 *
077 */
078public class OdfReferenceTableHelper extends AbstractLogEnabled implements Component, Serviceable
079{
080    /** Avalon Role */
081    public static final String ROLE = OdfReferenceTableHelper.class.getName();
082 
083    /** Abstract table ref */
084    public static final String ABSTRACT_TABLE_REF = "odf-enumeration.AbstractTableRef";
085    /** Domain */
086    public static final String DOMAIN = "odf-enumeration.Domain";
087    /** Degree */
088    public static final String DEGREE = "odf-enumeration.Degree";
089    /** Level */
090    public static final String LEVEL = "odf-enumeration.Level";
091    /** Program type. */
092    public static final String PROGRAM_TYPE = "odf-enumeration.ProgramType";
093    /** Form of teaching */
094    public static final String FORMOFTEACHING_METHOD = "odf-enumeration.FormofteachingMethod";
095    /** Orgnization of teaching */
096    public static final String FORMOFTEACHING_ORG = "odf-enumeration.FormofteachingOrg";
097    /** Teaching method */
098    public static final String TEACHING_ACTIVITY = "odf-enumeration.TeachingActivity";
099    /** Type of training course */
100    public static final String INTERNSHIP = "odf-enumeration.Internship";
101    /** Distance learning modalities */
102    public static final String DISTANCE_LEARNING_MODALITIES = "odf-enumeration.DistanceLearningModalities";
103    /** Place */
104    public static final String PLACE = "odf-enumeration.Place";
105    /** Teaching term. */
106    public static final String TEACHING_TERM = "odf-enumeration.TeachingTerm";
107    /** Time slot */
108    public static final String TIME_SLOT = "odf-enumeration.TimeSlot";
109    /** Code ROME */
110    public static final String CODE_ROME = "odf-enumeration.CodeRome";
111    /** Code ERASMUS */
112    public static final String CODE_ERASMUS = "odf-enumeration.CodeErasmus";
113    /** Code DGESIP */
114    public static final String CODE_DGESIP = "odf-enumeration.CodeDgesip";
115    /** Code SISE */
116    public static final String CODE_SISE = "odf-enumeration.CodeSise";
117    /** Code Cite97 */
118    public static final String CODE_CITE97 = "odf-enumeration.CodeCite97";
119    /** Code FAP */
120    public static final String CODE_FAP = "odf-enumeration.CodeFap";
121    /** Code NSF */
122    public static final String CODE_NSF = "odf-enumeration.CodeNsf";
123    /** RNCP level */
124    public static final String RNCP_LEVEL = "odf-enumeration.RncpLevel";
125    /** Join orgunit*/
126    public static final String JOIN_ORGUNIT = "odf-enumeration.JoinOrgunit";
127    /** Mention licence */
128    public static final String ABSTRACT_MENTION = "odf-enumeration.Mention";
129    /** Mention BUT */
130    public static final String MENTION_BUT = "odf-enumeration.MentionBUT";
131    /** Mention BUT */
132    public static final String MENTION_BUT_ATTRIBUTE_PARCOURS = "parcours";
133    /** Mention licence */
134    public static final String MENTION_LICENCE = "odf-enumeration.MentionLicence";
135    /** Mention licence pro */
136    public static final String MENTION_LICENCEPRO = "odf-enumeration.MentionLicencepro";
137    /** Mention master */
138    public static final String MENTION_MASTER = "odf-enumeration.MentionMaster";
139    /** Abstract table ref for category */
140    public static final String ABSTRACT_TABLE_REF_CATEGORY = "odf-enumeration.AbstractTableRefCategory";
141    /** Apprenticeship contract */
142    public static final String APPRENTICESHIP_CONTRACT = "odf-enumeration.ApprenticeshipContract";
143    /** Available certification */
144    public static final String AVAILABLE_CERTIFICATION = "odf-enumeration.AvailableCertification";
145    /** Campus */
146    public static final String CAMPUS = "odf-enumeration.Campus";
147    /** Category for code Erasmus */
148    public static final String CODE_ERASMUS_CATEGORY = "odf-enumeration.CodeErasmusCategory";
149    /** Category for code FAP */
150    public static final String CODE_FAP_CATEGORY = "odf-enumeration.CodeFapCategory";
151    /** Nature of container */
152    public static final String CONTAINER_NATURE = "odf-enumeration.ContainerNature";
153    /** Nature of course */
154    public static final String COURSE_NATURE = "odf-enumeration.CourseNature";
155    /** Discipline */
156    public static final String DISCIPLINE = "odf-enumeration.Discipline";
157    /** Duration */
158    public static final String DURATION = "odf-enumeration.Duration";
159    /** ECTS */
160    public static final String ECTS = "odf-enumeration.Ects";
161    /** Foreign place */
162    public static final String FOREIGN_PLACE = "odf-enumeration.ForeignPlace";
163    /** International education */
164    public static final String INTERNATIONAL_EDUCATION = "odf-enumeration.InternationalEducation";
165    /** Language */
166    public static final String LANGUAGE = "odf-enumeration.Language";
167    /** OrgUnit type */
168    public static final String ORGUNIT_TYPE = "odf-enumeration.OrgUnitType";
169    /** Period */
170    public static final String PERIOD = "odf-enumeration.Period";
171    /** Period type */
172    public static final String PERIOD_TYPE = "odf-enumeration.PeriodType";
173    /** Person role */
174    public static final String PERSON_ROLE = "odf-enumeration.PersonRole";
175    /** Program field */
176    public static final String PROGRAM_FIELD = "odf-enumeration.ProgramField";
177    /** Sectors */
178    public static final String SECTORS = "odf-enumeration.Sectors";
179    /** Nature of course part */
180    public static final String ENSEIGNEMENT_NATURE = "odf-enumeration.EnseignementNature";
181    /** Category of nature of course part */
182    public static final String ENSEIGNEMENT_NATURE_CATEGORY = "odf-enumeration.EnseignementNatureCategory";
183    /** Skill */
184    public static final String SKILL = "odf-enumeration.Skill";
185    /** Skill set */
186    public static final String SKILL_SET = "odf-enumeration.SkillSet";
187    /** Attribute name for mention type in table refe degree */
188    public static final String DEGREE_MENTION_TYPE = "mentionType";
189    
190    private AmetysObjectResolver _resolver;
191
192    private ContentTypeExtensionPoint _cTypeEP;
193
194    private ContentTypesHelper _cTypeHelper;
195
196    private ODFHelper _odfHelper;
197
198    @Override
199    public void service(ServiceManager manager) throws ServiceException
200    {
201        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
202        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
203        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
204        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
205    }
206    
207    /**
208     * Determines if the content type is a ODF table reference
209     * @param cTypeId The id of content type
210     * @return true if the content type is a ODF table reference
211     */
212    public boolean isTableReference(String cTypeId)
213    {
214        return _cTypeHelper.getAncestors(cTypeId).contains(ABSTRACT_TABLE_REF);
215    }
216    
217    /**
218     * Determines if the content is an entry of a ODF table reference
219     * @param content The content
220     * @return <code>true</code> if the content is an entry of a ODF table reference
221     */
222    public boolean isTableReferenceEntry(Content content)
223    {
224        String[] cTypeIds = content.getTypes();
225        for (String cTypeId : cTypeIds)
226        {
227            if (isTableReference(cTypeId))
228            {
229                return true;
230            }
231        }
232        return false;
233    }
234    
235    /**
236     * Get the id of table references
237     * @return The content type's id
238     */
239    public Set<String> getTableReferenceIds()
240    {
241        Set<String> tableRefIds = new HashSet<>();
242        
243        for (String cTypeId : _cTypeEP.getExtensionsIds())
244        {
245            if (_cTypeHelper.getAncestors(cTypeId).contains(ABSTRACT_TABLE_REF))
246            {
247                tableRefIds.add(cTypeId);
248            }
249        }
250        return tableRefIds;
251    }
252    
253    /**
254     * Get the attribute definitions for table references for a given content type
255     * @param cTypeId The id of content type
256     * @return The attribute definitions for table references
257     */
258    public Map<String, ContentAttributeDefinition> getTableRefAttributeDefinitions(String cTypeId)
259    {
260        ContentType cType = _cTypeEP.getExtension(cTypeId);
261        return _getTableRefAttributes(cType);
262    }
263    
264    private Map<String, ContentAttributeDefinition> _getTableRefAttributes(ModelItemContainer modelItemContainer)
265    {
266        Map<String, ContentAttributeDefinition> tableRefAttributes = new LinkedHashMap<>();
267        
268        for (ModelItem modelItem : modelItemContainer.getModelItems())
269        {
270            if (modelItem instanceof ContentAttributeDefinition)
271            {
272                ContentAttributeDefinition attributeDefinition = (ContentAttributeDefinition) modelItem;
273                if (isTableReference(attributeDefinition.getContentTypeId()))
274                {
275                    tableRefAttributes.put(attributeDefinition.getPath(), attributeDefinition);
276                }
277            }
278            else if (modelItem instanceof ModelItemContainer && !(modelItem instanceof RepeaterDefinition))
279            {
280                // enumerated attributes in repeaters are not supported
281                tableRefAttributes.putAll(_getTableRefAttributes((ModelItemContainer) modelItem));
282            }
283        }
284        
285        return tableRefAttributes;
286    }
287    
288    /**
289     * Get the attribute definitions for table references for a given content type and a view
290     * @param cTypeId The id of content type
291     * @param viewName the name of the view. Cannot be null.
292     * @return The attributes definitions for table references
293     */
294    public Map<String, ContentAttributeDefinition> getTableRefAttributeDefinitions(String cTypeId, String viewName)
295    {
296        ContentType cType = _cTypeEP.getExtension(cTypeId);
297        
298        View view = cType.getView(viewName);
299        return _getTableRefAttributes(view);
300    }
301    
302    private Map<String, ContentAttributeDefinition> _getTableRefAttributes(ViewItemContainer viewItemContainer)
303    {
304        Map<String, ContentAttributeDefinition> tableRefAttributes = new LinkedHashMap<>();
305        
306        for (ViewItem viewItem : viewItemContainer.getViewItems())
307        {
308            if (viewItem instanceof ViewElement)
309            {
310                ElementDefinition definition = ((ViewElement) viewItem).getDefinition();
311                if (definition instanceof ContentAttributeDefinition)
312                {
313                    tableRefAttributes.put(definition.getPath(), (ContentAttributeDefinition) definition);
314                }
315            }
316            else if (viewItem instanceof ViewItemContainer)
317            {
318                tableRefAttributes.putAll(_getTableRefAttributes((ViewItemContainer) viewItem));
319            }
320        }
321        
322        return tableRefAttributes;
323    }
324    
325    /**
326     * Get the content type for mention for this degree
327     * @param degreeIds The ids of degrees
328     * @return A map with the id of content type or null if there is no mention for this degree
329     */
330    @Callable
331    public Map<String, String> getMentionContentTypes(List<String> degreeIds)
332    {
333        Map<String, String> mentionTypes = new HashMap<>();
334        for (String degreeId : degreeIds)
335        {
336            mentionTypes.put(degreeId, getMentionForDegree(degreeId));
337        }
338        return mentionTypes;
339    }
340    
341    /**
342     * Get the available BUT training paths for a BUT mention
343     * @param mentionId the id of BUT mention
344     * @return the BUT training paths
345     */
346    public ContentValue[] getBUTParcoursForMention(String mentionId)
347    {
348        Content content = _resolver.resolveById(mentionId);
349        if (_cTypeHelper.isInstanceOf(content, MENTION_BUT))
350        {
351            return content.getValue(MENTION_BUT_ATTRIBUTE_PARCOURS, false, new ContentValue[0]);
352        }
353        return new ContentValue[0];
354    }
355    
356    /**
357     * Get the available BUT training paths for a content
358     * @param contentId the content's id
359     * @param mentionId the id of mention. Can be null or empty
360     * @return the available BUT training paths
361     */
362    @Callable
363    public List<Map<String, String>> getBUTParcoursItems(String contentId, String mentionId)
364    {
365        ContentValue[] values = null;
366        if (StringUtils.isNotEmpty(mentionId))
367        {
368            // Get items from mention
369            values = getBUTParcoursForMention(mentionId);
370        }
371        else
372        {
373            // Get items from parent program
374            values = getBUTParcoursItems(contentId);
375        }
376        
377        if (values != null)
378        {
379            return Arrays.stream(values)
380                .map(ContentValue::getContent)
381                .map(c -> Map.of("id", c.getId(), "title", c.getTitle()))
382                .collect(Collectors.toList());
383        }
384        else
385        {
386            return List.of();
387        }
388    }
389    /**
390     * Get the available BUT training paths for a {@link ProgramItem}
391     * @param programItemId the id of program item
392     * @return the BUT training paths
393     */
394    public ContentValue[] getBUTParcoursItems(String programItemId)
395    {
396        Content content = _resolver.resolveById(programItemId);
397        
398        // Get first parent program
399        Set<Program> parentPrograms = _odfHelper.getParentPrograms((AbstractProgram) content);
400        if (!parentPrograms.isEmpty())
401        {
402            Program program = parentPrograms.iterator().next();
403            
404            if (isBUTDiploma(program))
405            {
406                return Optional.ofNullable(program.<ContentValue>getValue(AbstractProgram.MENTION))
407                    .flatMap(ContentValue::getContentIfExists)
408                    .map(c -> c.<ContentValue[]>getValue(MENTION_BUT_ATTRIBUTE_PARCOURS))
409                    .orElse(new ContentValue[0]);
410                
411            }
412        }
413        
414        // No BUT training paths
415        return new ContentValue[0];
416    }
417    
418    /**
419     * Determines if the program is a BUT diploma
420     * @param program the program
421     * @return true if the program has a BUT
422     */
423    public boolean isBUTDiploma(Program program)
424    {
425        String mentionType = getMentionType(program);
426        return mentionType != null && MENTION_BUT.equals(mentionType);
427    }
428    
429    /**
430     * Get the type of mention for a program
431     * @param program the program
432     * @return the type of mention or null if there is no mention for this program
433     */
434    public String getMentionType(Program program)
435    {
436        return Optional.ofNullable(program.<ContentValue>getValue(AbstractProgram.DEGREE))
437                .flatMap(ContentValue::getContentIfExists)
438                .map(c -> c.<String>getValue(DEGREE_MENTION_TYPE))
439                .orElse(null);
440    }
441    
442    /**
443     * Get the mention for a given degree.
444     * @param degreeId The degree ID
445     * @return The associated mention reference table, null if there isn't
446     */
447    public String getMentionForDegree(String degreeId)
448    {
449        try
450        {
451            Content content = _resolver.resolveById(degreeId);
452            return content.getValue(DEGREE_MENTION_TYPE);
453        }
454        catch (UnknownAmetysObjectException e)
455        {
456            // Nothing to do
457        }
458        return null;
459    }
460    
461    /**
462     * Get the CDM-fr value associated with the given code
463     * @param tableRefId The id of content type
464     * @param code The code
465     * @return The CDM-fr value or empty string if not found
466     */
467    public String getCDMfrValue (String tableRefId, String code)
468    {
469        ContentTypeExpression cTypeExpr = new ContentTypeExpression(Operator.EQ, tableRefId);
470        StringExpression codeExpr = new StringExpression(OdfReferenceTableEntry.CODE, Operator.EQ, code);
471        
472        String xpathQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, codeExpr));
473        AmetysObjectIterable<Content> contents = _resolver.query(xpathQuery);
474        AmetysObjectIterator<Content> it = contents.iterator();
475        
476        if (it.hasNext())
477        {
478            OdfReferenceTableEntry entry = new OdfReferenceTableEntry(it.next());
479            return entry.getCdmValue();
480        }
481        
482        return "";
483    }
484    
485    /**
486     * Get all items of an enumeration and their label
487     * @param tableRefId The id of content type
488     * @return items of enumeration
489     */
490    public List<OdfReferenceTableEntry> getItems (String tableRefId)
491    {
492        return getItems(tableRefId, new SortField[0]);
493    }
494    
495    /**
496     * Get all items of an enumeration and their label, sorted by given fields
497     * @param tableRefId The id of content type
498     * @param sortFields The sort fields to order results
499     * @return items of enumeration
500     */
501    public List<OdfReferenceTableEntry> getItems (String tableRefId, SortField... sortFields)
502    {
503        return getItems(tableRefId, true, sortFields);
504    }
505    
506    /**
507     * Get all items of an enumeration and their label, sorted by given fields
508     * @param tableRefId The id of content type
509     * @param includeArchived <code>true</code> to include archived entries
510     * @param sortFields The sort fields to order results
511     * @return items of enumeration
512     */
513    public List<OdfReferenceTableEntry> getItems (String tableRefId, boolean includeArchived, SortField... sortFields)
514    {
515        Expression expr = new ContentTypeExpression(Operator.EQ, tableRefId);
516        if (!includeArchived)
517        {
518            Expression notArchivedExpr = new NotExpression(new BooleanExpression(OdfReferenceTableEntry.ARCHIVED, true));
519            expr = new AndExpression(expr, notArchivedExpr);
520        }
521        
522        SortCriteria sortCriteria = new SortCriteria();
523        for (SortField sortField : sortFields)
524        {
525            sortCriteria.addCriterion(sortField.getName(), sortField.getAscending(), sortField.getNormalize());
526        }
527        
528        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria);
529        AmetysObjectIterable<Content> contents = _resolver.query(xpathQuery);
530        
531        return contents.stream().map(OdfReferenceTableEntry::new).collect(Collectors.toList());
532    }
533    
534    /**
535     * Returns the label of an reference table entry
536     * @param contentId The content id
537     * @param lang The requested language of label
538     * @return the item label or <code>null</code> if not found
539     */
540    public String getItemLabel(String contentId, String lang)
541    {
542        try
543        {
544            Content content = _resolver.resolveById(contentId);
545            OdfReferenceTableEntry entry = new OdfReferenceTableEntry(content);
546            return entry.getLabel(lang);
547        }
548        catch (UnknownAmetysObjectException e)
549        {
550            return null;
551        }
552    }
553    
554    /**
555     * Returns the label of an reference table entry
556     * @param tableRefId The id of content type (useless)
557     * @param contentId The content id
558     * @param lang The requested language of label
559     * @return the item label or <code>null</code> if not found
560     * @deprecated Use {@link #getItemLabel(String, String)} instead
561     */
562    @Deprecated
563    public String getItemLabel (String tableRefId, String contentId, String lang)
564    {
565        return getItemLabel(contentId, lang);
566    }
567
568    /**
569     * Returns the CMD value of an reference table entry
570     * @param contentId The content id
571     * @param returnCodeIfEmpty <code>true</code> to return the code if CDM-fr value is empty
572     * @return the CDM-fr value or empty value if not found
573     */
574    public String getItemCDMfrValue(String contentId, boolean returnCodeIfEmpty)
575    {
576        if (StringUtils.isEmpty(contentId))
577        {
578            return "";
579        }
580        
581        Content content = _resolver.resolveById(contentId);
582        OdfReferenceTableEntry entry = new OdfReferenceTableEntry(content);
583        
584        String cdmValue = entry.getCdmValue();
585        
586        if (StringUtils.isEmpty(cdmValue) && returnCodeIfEmpty)
587        {
588            return entry.getCode();
589        }
590        return cdmValue;
591    }
592    
593    /**
594     * Returns the CMD value of an reference table entry
595     * @param tableRefId The id of content type (useless)
596     * @param contentId The content id
597     * @param returnCodeIfEmpty <code>true</code> to return the code if CDM-fr value is empty
598     * @return the CDM-fr value or empty value if not found
599     * @deprecated Use {@link #getItemCDMfrValue(String, boolean)} instead
600     */
601    @Deprecated
602    public String getItemCDMfrValue (String tableRefId, String contentId, boolean returnCodeIfEmpty)
603    {
604        return getItemCDMfrValue(contentId, returnCodeIfEmpty);
605    }
606    
607    /**
608     * Returns the code of an reference table entry from its CDM value
609     * @param contentId The id of content
610     * @return the code or empty value if not found
611     */
612    public String getItemCode(String contentId)
613    {
614        if (StringUtils.isEmpty(contentId))
615        {
616            return "";
617        }
618        
619        try
620        {
621            Content content = _resolver.resolveById(contentId);
622            OdfReferenceTableEntry entry = new OdfReferenceTableEntry(content);
623            return entry.getCode();
624        }
625        catch (UnknownAmetysObjectException e)
626        {
627            return "";
628        }
629    }
630    
631    /**
632     * Returns the code of an reference table entry from its CDM value
633     * @param tableRefId The id of content type (useless)
634     * @param contentId The id of content
635     * @return the code or empty value if not found
636     * @deprecated Use {@link #getItemCode(String)} instead
637     */
638    @Deprecated
639    public String getItemCode (String tableRefId, String contentId)
640    {
641        return getItemCode(contentId);
642    }
643    
644    /**
645     * Returns the code of an reference table entry from its CDM value
646     * @param tableRefId The id of content type
647     * @param cdmValue The CDM-fr value
648     * @return the code or <code>null</code> if not found
649     */
650    public String getItemCodeFromCDM (String tableRefId, String cdmValue)
651    {
652        ContentTypeExpression cTypeExpr = _getContentTypeExpression(tableRefId);
653        StringExpression codeExpr = new StringExpression(OdfReferenceTableEntry.CDM_VALUE, Operator.EQ, cdmValue);
654        
655        String xpathQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, codeExpr));
656        AmetysObjectIterable<Content> contents = _resolver.query(xpathQuery);
657        AmetysObjectIterator<Content> it = contents.iterator();
658        
659        if (it.hasNext())
660        {
661            OdfReferenceTableEntry entry = new OdfReferenceTableEntry(it.next());
662            return entry.getCode();
663        }
664        return null;
665    }
666
667    /**
668     * Returns the entry of an reference table entry from its cdmValue
669     * @param tableRefId The id of content type
670     * @param cdmValue The CDM-fr value
671     * @return the entry or <code>null</code> if not found
672     */
673    public OdfReferenceTableEntry getItemFromCDM(String tableRefId, String cdmValue)
674    {
675        ContentTypeExpression cTypeExpr = _getContentTypeExpression(tableRefId);
676        StringExpression cdmExpr = new StringExpression(OdfReferenceTableEntry.CDM_VALUE, Operator.EQ, cdmValue);
677        
678        String xpathQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, cdmExpr));
679
680        return _resolver.<Content>query(xpathQuery).stream()
681            .findFirst()
682            .map(OdfReferenceTableEntry::new)
683            .orElse(null);
684    }
685    
686    /**
687     * Returns the entry of an reference table entry from its code
688     * @param tableRefId The id of content type
689     * @param code The code
690     * @return the entry or <code>null</code> if not found
691     */
692    public OdfReferenceTableEntry getItemFromCode(String tableRefId, String code)
693    {
694        ContentTypeExpression cTypeExpr = _getContentTypeExpression(tableRefId);
695        StringExpression codeExpr = new StringExpression(OdfReferenceTableEntry.CODE, Operator.EQ, code);
696        
697        String xpathQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, codeExpr));
698        
699        return _resolver.<Content>query(xpathQuery).stream()
700            .findFirst()
701            .map(OdfReferenceTableEntry::new)
702            .orElse(null);
703    }
704    
705    /**
706     * Returns the entry of a reference table entry from its code for the connector (Apogée, Pégase...)
707     * @param tableRefId The id of content type
708     * @param connectorCode The code
709     * @param codeFieldName The field name containing the connector code
710     * @return the entry or <code>null</code> if not found
711     */
712    public OdfReferenceTableEntry getItemFromConnector(String tableRefId, String connectorCode, String codeFieldName)
713    {
714        ContentTypeExpression cTypeExpr = _getContentTypeExpression(tableRefId);
715        StringExpression cdmExpr = new StringExpression(codeFieldName, Operator.EQ, connectorCode);
716        
717        String xpathQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, cdmExpr));
718
719        return _resolver.<Content>query(xpathQuery).stream()
720            .findFirst()
721            .map(OdfReferenceTableEntry::new)
722            .orElse(null);
723    }
724    
725    private ContentTypeExpression _getContentTypeExpression(String tableRefId)
726    {
727        Set<String> tableRefids;
728        if (tableRefId.equals(OdfReferenceTableHelper.ABSTRACT_MENTION))
729        {
730            tableRefids = _cTypeEP.getSubTypes(tableRefId);
731        }
732        else
733        {
734            tableRefids = Collections.singleton(tableRefId);
735        }
736        
737        return new ContentTypeExpression(Operator.EQ, tableRefids.toArray(new String[tableRefids.size()]));
738    }
739
740    /**
741     * Returns the reference table entry from its CDM value
742     * @param contentId The id of content
743     * @return the item as an {@link OdfReferenceTableEntry} or null if not found
744     */
745    public OdfReferenceTableEntry getItem(String contentId)
746    {
747        try
748        {
749            Content content = _resolver.resolveById(contentId);
750            return new OdfReferenceTableEntry(content);
751        }
752        catch (UnknownAmetysObjectException e)
753        {
754            // Can be an empty ID or an invalid ID (workspace or simply deleted element)
755            return null;
756        }
757    }
758
759    /**
760     * SAX items of a reference table
761     * @param contentHandler The content handler to sax into
762     * @param tableRefId The id of reference table
763     * @throws SAXException if an error occurred while saxing
764     */
765    public void saxItems (ContentHandler contentHandler, String tableRefId) throws SAXException
766    {
767        saxItems(contentHandler, tableRefId, null);
768    }
769    
770    /**
771     * SAX items of a reference table
772     * @param contentHandler the content handler to sax into
773     * @param attributeDefinition the metadata definition
774     * @throws SAXException if an error occurs while saxing
775     */
776    public void saxItems (ContentHandler contentHandler, ContentAttributeDefinition attributeDefinition) throws SAXException
777    {
778        saxItems(contentHandler, attributeDefinition, null);
779    }
780    
781    /**
782     * SAX items of a reference table
783     * @param contentHandler The content handler to sax into
784     * @param tableRefId The id of reference table
785     * @param lang the language to use to display items, can be <code>null</code> then odf language is used
786     * @throws SAXException if an error occurred while saxing
787     */
788    public void saxItems (ContentHandler contentHandler, String tableRefId, String lang) throws SAXException
789    {
790        _saxItems(contentHandler, new AttributesImpl(), tableRefId, lang);
791    }
792    
793    /**
794     * SAX items of a reference table
795     * @param contentHandler the content handler to sax into
796     * @param attributeDefinition the metadata definition
797     * @param lang the language to use to display items, can be <code>null</code> then odf language is used
798     * @throws SAXException if an error occurs while saxing
799     */
800    public void saxItems(ContentHandler contentHandler, ContentAttributeDefinition attributeDefinition, String lang) throws SAXException
801    {
802        String cTypeId = attributeDefinition.getContentTypeId();
803        if (cTypeId.startsWith("odf-enumeration."))
804        {
805            AttributesImpl attrs = new AttributesImpl();
806            attrs.addCDATAAttribute("metadataName", attributeDefinition.getName());
807            attrs.addCDATAAttribute("metadataPath", attributeDefinition.getPath());
808            
809            _saxItems(contentHandler, attrs, cTypeId, lang);
810        }
811    }
812    
813    private void _saxItems(ContentHandler contentHandler, AttributesImpl rootAttrs, String tableRefId, String lang) throws SAXException
814    {
815        String langToUse = lang != null ? lang : Config.getInstance().getValue("odf.programs.lang");
816        boolean hasShortLabel = _cTypeEP.getExtension(tableRefId).hasModelItem("shortLabel");
817        
818        rootAttrs.addCDATAAttribute("contentTypeId", tableRefId);
819        XMLUtils.startElement(contentHandler, "items", rootAttrs);
820        
821        List<OdfReferenceTableEntry> entries = getItems(tableRefId);
822        for (OdfReferenceTableEntry entry : entries)
823        {
824            AttributesImpl valueAttrs = new AttributesImpl();
825            valueAttrs.addCDATAAttribute("id", entry.getId());
826            valueAttrs.addCDATAAttribute("order", entry.getOrder().toString());
827            valueAttrs.addCDATAAttribute("code", entry.getCode());
828            valueAttrs.addCDATAAttribute("cdmValue", entry.getCdmValue());
829            valueAttrs.addCDATAAttribute("archived", entry.isArchived().toString());
830            if (hasShortLabel)
831            {
832                valueAttrs.addCDATAAttribute("shortLabel", entry.getContent().getValue("shortLabel", false, StringUtils.EMPTY));
833            }
834            
835            XMLUtils.createElement(contentHandler, "item", valueAttrs, entry.getLabel(langToUse));
836        }
837        
838        XMLUtils.endElement(contentHandler, "items");
839    }
840    
841    /**
842     * This class represents a sort field for reference table.
843     *
844     */
845    public static final class SortField
846    {
847        private String _name;
848        private boolean _ascending;
849        private boolean _normalize;
850
851        /**
852         * Create a sort field
853         * @param name the name of field to sort on
854         * @param ascending <code>true</code> to sort in ascending order, <code>false</code> otherwise
855         */
856        public SortField(String name, boolean ascending)
857        {
858            this(name, ascending, false);
859        }
860        
861        /**
862         * Create a sort field
863         * @param name the name of field to sort on
864         * @param ascending <code>true</code> to sort in ascending order, <code>false</code> otherwise
865         * @param normalize <code>true</code> to normalize string properties (remove accents and lower case)
866         */
867        public SortField(String name, boolean ascending, boolean normalize)
868        {
869            _name = name;
870            _ascending = ascending;
871            _normalize = normalize;
872        }
873
874        /**
875         * Get the name of the sort field
876         * @return the name of the sort field
877         */
878        public String getName()
879        {
880            return _name;
881        }
882
883        /**
884         * Get the order for sorting results
885         * @return <code>true</code> to sort results in ascending order, <code>false</code> otherwise
886         */
887        public boolean getAscending()
888        {
889            return _ascending;
890        }
891        
892        /**
893         * Return the normalize status for this sort field
894         * @return <code>true</code> if string properties should be normalized (remove accents and lower case)
895         */
896        public boolean getNormalize()
897        {
898            return _normalize;
899        }
900
901    }
902}