001/*
002 *  Copyright 2024 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.properties.section.content;
017
018import java.util.ArrayList;
019import java.util.Comparator;
020import java.util.HashMap;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Optional;
026import java.util.function.Function;
027import java.util.function.Predicate;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.LocaleUtils;
033
034import org.ametys.cms.properties.section.AbstractDefaultPropertySection;
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.repository.Content.ReferencingContentsSearch;
037import org.ametys.odf.ODFHelper;
038import org.ametys.odf.ProgramItem;
039import org.ametys.odf.course.Course;
040import org.ametys.odf.coursepart.CoursePart;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043
044/**
045 * The referencer items for {@link ProgramItem}.
046 * It displays educational paths, children and other references to the content.
047 */
048public class ProgramItemReferencerSection extends AbstractDefaultPropertySection implements Serviceable
049{
050    private static final ListProgramItemComparator __LIST_PROGRAM_ITEM_COMPARATOR = new ListProgramItemComparator();
051
052    private AmetysObjectResolver _resolver;
053    private ODFHelper _odfHelper;
054
055    public void service(ServiceManager smanager) throws ServiceException
056    {
057        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
058        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
059    }
060    
061    public boolean supports(AmetysObject ametysObject)
062    {
063        return ametysObject instanceof ProgramItem;
064    }
065    
066    @Override
067    protected Map<String, Object> buildData(AmetysObject ametysObject)
068    {
069        ProgramItem programItem = (ProgramItem) ametysObject;
070        
071        Map<String, Object> resultMap = new LinkedHashMap<>();
072
073        resultMap.put("educationalPaths", _getEducationalPaths(programItem));
074        resultMap.put("children", _getChildren(programItem));
075        resultMap.put("otherReferences", _getOtherReferences((Content) ametysObject));
076        
077        return resultMap;
078    }
079
080    private List<List<Map<String, Object>>> _getEducationalPaths(ProgramItem programItem)
081    {
082        // To avoid to return path only with the program item itself
083        if (_odfHelper.hasParentProgramItems(programItem))
084        {
085            return _odfHelper.getEducationalPaths(programItem)
086                .stream()
087                .map(p -> p.getProgramItems(_resolver))
088                .sorted(__LIST_PROGRAM_ITEM_COMPARATOR)
089                .map(this::_programItemList2JSON)
090                .toList();
091        }
092        
093        return List.of();
094    }
095    
096    private List<Map<String, Object>> _getChildren(ProgramItem programItem)
097    {
098        List<Map<String, Object>> children = new ArrayList<>();
099        
100        // Add child program items to children
101        children.addAll(_programItemList2JSON(_odfHelper.getChildProgramItems(programItem)));
102        
103        // Add course parts to children only if the content is a course
104        if (programItem instanceof Course course)
105        {
106            children.addAll(_coursePartList2JSON(course.getCourseParts()));
107        }
108        
109        return children;
110    }
111
112    private Map<String, Object> _getOtherReferences(Content content)
113    {
114        Map<String, Object> resultMap = new LinkedHashMap<>();
115        
116        ReferencingContentsSearch referencingContents = content.searchReferencingContents(100);
117
118        // Build the list of already referenced contents
119        List<Object> alreadyReferencedContents = new ArrayList<>();
120        alreadyReferencedContents.addAll(_odfHelper.getChildProgramItems((ProgramItem) content));
121        if (content instanceof Course course)
122        {
123            alreadyReferencedContents.addAll(course.getCourseParts());
124        }
125        alreadyReferencedContents.addAll(_odfHelper.getParentProgramItems((ProgramItem) content));
126        
127        Locale locale = Optional.of(content)
128            .map(Content::getLanguage)
129            .map(LocaleUtils::toLocale)
130            .orElse(null);
131        
132        resultMap.put(
133            "referencingContents",
134            referencingContents.referencingContents()
135                .stream()
136                // Remove already referenced contents (direct children and direct parents)
137                .filter(Predicate.not(alreadyReferencedContents::contains))
138                .map(c -> _content2JSON(c, locale))
139                .toList()
140        );
141        
142        if (referencingContents.hasOtherReferences())
143        {
144            resultMap.put("hasOther", true);
145        }
146        return resultMap;
147    }
148    
149    private List<Map<String, Object>> _programItemList2JSON(List<ProgramItem> programItems)
150    {
151        return _odfItemList2JSON(programItems, c -> ((ProgramItem) c).getDisplayCode());
152    }
153    
154    private List<Map<String, Object>> _coursePartList2JSON(List<CoursePart> courseParts)
155    {
156        return _odfItemList2JSON(courseParts, c -> ((CoursePart) c).getDisplayCode());
157    }
158    
159    private List<Map<String, Object>> _odfItemList2JSON(List<?> contents, Function<Content, String> getDisplayCodeFn)
160    {
161        return contents.stream()
162                .map(Content.class::cast)
163                .map(c -> _odfItem2JSON(c, getDisplayCodeFn))
164                .toList();
165    }
166    
167    private Map<String, Object> _odfItem2JSON(Content content, Function<Content, String> getDisplayCodeFn)
168    {
169        Map<String, Object> json = new HashMap<>();
170        json.put("id", content.getId());
171        json.put("title", content.getTitle());
172        json.put("code", getDisplayCodeFn.apply(content));
173        return json;
174    }
175    
176    private Map<String, Object> _content2JSON(Content content, Locale locale)
177    {
178        return Map.of(
179            "id", content.getId(),
180            "title", content.getTitle(locale)
181        );
182    }
183
184    /**
185     * Comparator of {@link List} composing of {@link ProgramItem} by title.
186     */
187    private static final class ListProgramItemComparator implements Comparator<List<ProgramItem>>
188    {
189        public int compare(List<ProgramItem> l1, List<ProgramItem> l2)
190        {
191            int l1Size = l1.size();
192            int l2Size = l2.size();
193            
194            for (int i = 0; i < l1Size; i++)
195            {
196                if (l2Size <= i)
197                {
198                    // l1 is greater than l2
199                    return 1;
200                }
201                else
202                {
203                    // Compare title
204                    int comparing = ((Content) l1.get(i)).getTitle().compareTo(((Content) l2.get(i)).getTitle());
205                    if (comparing != 0)
206                    {
207                        return comparing;
208                    }
209                }
210            }
211            
212            // Lists are the same at the end of reading l1
213            // Then compare size of the lists
214            return Integer.compare(l1Size, l2Size);
215        }
216    }
217}