001/*
002 *  Copyright 2019 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.ose.export.utils;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.List;
021import java.util.Objects;
022import java.util.Optional;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030
031import org.ametys.cms.data.ContentValue;
032import org.ametys.cms.repository.Content;
033import org.ametys.odf.ODFHelper;
034import org.ametys.odf.ProgramItem;
035import org.ametys.odf.course.Course;
036import org.ametys.odf.enumeration.OdfReferenceTableEntry;
037import org.ametys.odf.enumeration.OdfReferenceTableHelper;
038import org.ametys.odf.orgunit.OrgUnit;
039import org.ametys.odf.ose.export.OSEConstants;
040import org.ametys.odf.ose.export.impl.odf.LogUtils;
041import org.ametys.odf.program.AbstractProgram;
042import org.ametys.odf.program.Container;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.runtime.model.ModelHelper;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048/**
049 * A retriever used like a helper to retrieve some ODF elements from ODF items.
050 */
051public class ElementRetriever extends AbstractLogEnabled implements Component, Serviceable
052{
053    /** Avalon Role */
054    public static final String ROLE = ElementRetriever.class.getName();
055
056    /** The helper for ODF contents */
057    protected ODFHelper _odfHelper;
058    /** The helper for reference tables from ODF */
059    protected OdfReferenceTableHelper _refTableHelper;
060    /** The resolver */
061    protected AmetysObjectResolver _resolver;
062
063    @Override
064    public void service(ServiceManager manager) throws ServiceException
065    {
066        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
067        _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
068        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
069    }
070
071    /**
072     * Retrieve the steps ({@link Container}s of nature 'annee') of a given {@link ProgramItem}
073     * @param programItem The program item
074     * @return the steps ({@link Container}s of nature 'annee') of the given program item
075     */
076    public Set<Container> retrieveSteps(ProgramItem programItem)
077    {
078        if (_mustNotExport(programItem))
079        {
080            return Collections.EMPTY_SET;
081        }
082        
083        return _odfHelper.getParentProgramItems(programItem)
084                .stream()
085                .map(this::_getSelfAsStepOrParentSteps)
086                .flatMap(Set::stream)
087                .collect(Collectors.toSet());
088    }
089    
090    private boolean _mustNotExport(ProgramItem programItem)
091    {
092        if (programItem instanceof Course || programItem instanceof Container)
093        {
094            return ((Content) programItem).getValue(OSEConstants.NO_OSE_EXPORT_ATTRIBUTE_NAME, false, false);
095        }
096        else
097        {
098            return false;
099        }
100    }
101    
102    private Set<Container> _getSelfAsStepOrParentSteps(ProgramItem programItem)
103    {
104        return Optional.of(programItem)
105                .filter(this::_isStep)
106                .map(Container.class::cast)
107                .map(Collections::singleton) // Get programItem if it is a step
108                .orElseGet(() -> retrieveSteps(programItem)); // Otherwise search in the parents
109    }
110
111    /**
112     * Retrieve the {@link OrgUnit}s of a given {@link ProgramItem}
113     * @param programItem The program item
114     * @return the {@link OrgUnit}s of the given program item
115     */
116    public Set<OrgUnit> retrieveOrgUnits(ProgramItem programItem)
117    {
118        return _retrieveOrgUnits(programItem);
119    }
120    
121    private Set<OrgUnit> _retrieveOrgUnits(ProgramItem parent)
122    {
123        return Optional.of(parent)
124                .map(this::_getDirectOrgUnit)
125                .map(Collections::singleton)
126                .orElseGet(() -> _retrieveOrgUnitsFromParents(parent));
127    }
128    
129    private Set<OrgUnit> _retrieveOrgUnitsFromParents(ProgramItem programItem)
130    {
131        return _odfHelper.getParentProgramItems(programItem)
132                .stream()
133                .map(this::_retrieveOrgUnits)
134                .flatMap(Set::stream)
135                .collect(Collectors.toSet());
136    }
137    
138    private OrgUnit _getDirectOrgUnit(ProgramItem programItem)
139    {
140        if (programItem instanceof Course)
141        {
142            List<String> orgUnitIds = ((Course) programItem).getOrgUnits();
143            return _resolveAllAndGetFirst(orgUnitIds, (Course) programItem);
144        }
145        else if (programItem instanceof AbstractProgram)
146        {
147            List<String> orgUnitIds = ((AbstractProgram) programItem).getOrgUnits();
148            return _resolveAllAndGetFirst(orgUnitIds, (AbstractProgram) programItem);
149        }
150        else if (programItem instanceof Container)
151        {
152            return Optional.of(programItem)
153                    .map(Container.class::cast)
154                    .map(Container::getOrgUnit)
155                    .filter(Objects::nonNull)
156                    .map(this::_resolve)
157                    .filter(Objects::nonNull)
158                    .orElse(null);
159        }
160        return null;
161    }
162    
163    private <T extends ProgramItem & Content> OrgUnit _resolveAllAndGetFirst(Collection<String> orgUnitIds, T programElement)
164    {
165        List<OrgUnit> orgUnits = orgUnitIds.stream()
166                .map(this::_resolve)
167                .filter(Objects::nonNull)
168                .collect(Collectors.toList());
169        
170        if (orgUnits.size() > 1)
171        {
172            LogUtils.programElementWarningOrgUnits(getLogger(), programElement, orgUnits);
173        }
174        
175        return orgUnits.isEmpty() ? null : orgUnits.get(0);
176    }
177    
178    private OrgUnit _resolve(String id)
179    {
180        try
181        {
182            return _resolver.resolveById(id);
183        }
184        catch (AmetysRepositoryException e)
185        {
186            // Silently fail
187            return null;
188        }
189    }
190
191    /**
192     * Retrieve the degrees of a given {@link Container}
193     * @param container The container
194     * @return the degrees of the given container
195     */
196    public Set<OdfReferenceTableEntry> retrieveDegree(Container container)
197    {
198        return container.getRootPrograms()
199                .stream()
200                .map(p -> p.<ContentValue>getValue(AbstractProgram.DEGREE))
201                .filter(Objects::nonNull)
202                .map(ContentValue::getContentIfExists)
203                .filter(Optional::isPresent)
204                .map(Optional::get)
205                .map(OdfReferenceTableEntry::new)
206                .collect(Collectors.toSet());
207    }
208
209    /**
210     * Get the potential steps holder of the program item. A step is a container of year type and it can be set manually on intermediate courses.
211     * @param programItem The {@link ProgramItem} on which we have to retrieve the potential steps holder
212     * @return the potential steps holder of the program item
213     */
214    public Set<Container> retrieveStepsHolder(ProgramItem programItem)
215    {
216        return _getStepHolderFromCourse(programItem) // If the element is a course and has a step holder
217            .or(() -> _getStepHolderFromContainer(programItem)) // If the element is a step (container of type year)
218            .map(Collections::singleton)
219            .orElseGet(() -> _getStepsHolderFromParentElements(programItem)); // In all other cases, search in the parent elements
220    }
221    
222    private Optional<Container> _getStepHolderFromCourse(ProgramItem programItem)
223    {
224        return Optional.of(programItem)
225                .filter(Course.class::isInstance)
226                .map(Course.class::cast)
227                .map(c -> c.<ContentValue>getValue("etapePorteuse"))
228                .flatMap(ContentValue::getContentIfExists)
229                .map(Container.class::cast);
230    }
231    
232    private Optional<Container> _getStepHolderFromContainer(ProgramItem programItem)
233    {
234        return Optional.of(programItem)
235                .filter(this::_isStep)
236                .map(Container.class::cast);
237    }
238
239    private boolean _isStep(ProgramItem programItem)
240    {
241        return _isContainerOfNature(programItem, "annee");
242    }
243    
244    private Set<Container> _getStepsHolderFromParentElements(ProgramItem programItem)
245    {
246        return _odfHelper.getParentProgramItems(programItem)
247                .stream()
248                .map(this::retrieveStepsHolder)
249                .flatMap(Set::stream)
250                .collect(Collectors.toSet());
251    }
252    
253    /**
254     * Get the potential period types of the program item. It can be retrieved on courses or containers. The algorithm doesn't search in the parent of semester containers.
255     * @param programItem The {@link ProgramItem} on which we have to retrieve the potential period types
256     * @return the potential period types of the program item
257     */
258    public Set<OdfReferenceTableEntry> retrievePeriodTypes(ProgramItem programItem)
259    {
260        return _getPeriodType(programItem) // Try to get the period type on the current program item
261            .map(Collections::singleton)
262            .orElseGet(() -> _searchPeriodTypesInParents(programItem)); // Or search in the parent program items
263    }
264
265    private Set<OdfReferenceTableEntry> _searchPeriodTypesInParents(ProgramItem programItem)
266    {
267        if (_isSemester(programItem))
268        {
269            return Collections.EMPTY_SET;
270        }
271        
272        // Only if programItem not a semester
273        return _odfHelper.getParentProgramItems(programItem) // Get the direct parent program items
274            .stream()
275            .map(this::retrievePeriodTypes) // Retrieve the period types for all parents
276            .flatMap(Set::stream)
277            .collect(Collectors.toSet());
278    }
279    
280    private Optional<OdfReferenceTableEntry> _getPeriodType(ProgramItem programItem)
281    {
282        return Optional.of(programItem)
283            .map(Content.class::cast) // Cast to Content (ProgramItem is always a Content)
284            .filter(c -> ModelHelper.hasModelItem("period", c.getModel())) // Filter if the attribute "period" is not in the model
285            .map(c -> c.<ContentValue>getValue("period")) // Get the period
286            .flatMap(ContentValue::getContentIfExists)
287            .map(c -> c.<ContentValue>getValue("type")) // Get the period type
288            .flatMap(ContentValue::getContentIfExists)
289            .map(OdfReferenceTableEntry::new);
290    }
291
292    private boolean _isSemester(ProgramItem programItem)
293    {
294        return _isContainerOfNature(programItem, "semestre");
295    }
296
297    private boolean _isContainerOfNature(ProgramItem programItem, String natureCode)
298    {
299        return Optional.of(programItem)
300            .filter(Container.class::isInstance)
301            .map(Container.class::cast)
302            .map(Container::getNature)
303            .map(_refTableHelper::getItemCode)
304            .map(natureCode::equals)
305            .orElse(false);
306    }
307}