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.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.commons.lang3.StringUtils;
029
030import org.ametys.cms.data.ContentValue;
031import org.ametys.cms.repository.Content;
032import org.ametys.core.cache.Cache;
033import org.ametys.odf.ProgramItem;
034import org.ametys.odf.course.Course;
035import org.ametys.odf.enumeration.OdfReferenceTableEntry;
036import org.ametys.odf.enumeration.OdfReferenceTableHelper;
037import org.ametys.odf.orgunit.OrgUnit;
038import org.ametys.odf.ose.export.OSEConstants;
039import org.ametys.odf.ose.export.impl.odf.LogUtils;
040import org.ametys.odf.program.AbstractProgram;
041import org.ametys.odf.program.Container;
042import org.ametys.plugins.odfpilotage.helper.PilotageHelper;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.model.ModelHelper;
047
048/**
049 * A retriever used like a helper to retrieve some ODF elements from ODF items.
050 */
051public class ElementRetriever extends PilotageHelper
052{
053    /** Avalon Role */
054    @SuppressWarnings("hiding")
055    public static final String ROLE = ElementRetriever.class.getName();
056    
057    private static final String __STEPS_BY_ITEM_CACHE_ID = ElementRetriever.class.getName() + "$stepsByItem";
058
059    /** The resolver */
060    protected AmetysObjectResolver _resolver;
061
062    @Override
063    public void service(ServiceManager manager) throws ServiceException
064    {
065        super.service(manager);
066        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
067    }
068
069    @Override
070    public void initialize() throws Exception
071    {
072        if (!_cacheManager.hasCache(__STEPS_BY_ITEM_CACHE_ID)) 
073        {
074            _cacheManager.createRequestCache(__STEPS_BY_ITEM_CACHE_ID,
075                new I18nizableText("plugin.odf-ose", "PLUGINS_ODF_OSE_CACHE_STEPS_BY_ITEM_LABEL"),
076                new I18nizableText("plugin.odf-ose", "PLUGINS_ODF_OSE_CACHE_STEPS_BY_ITEM_DESCRIPTION"),
077                false
078            );
079        }
080    }
081    
082    @Override
083    public Set<Container> getSteps(ProgramItem programItem)
084    {
085        // The cache id is different than its superclass
086        return getYearId()
087                .map(yearId -> _getSteps(programItem, yearId, _cacheManager.get(__STEPS_BY_ITEM_CACHE_ID)))
088                .orElseGet(() -> Set.of());
089    }
090    
091    @Override
092    protected Set<Container> _getStepsToCache(ProgramItem programItem, String yearId, Cache<String, Set<Container>> cache)
093    {
094        return _mustNotExport(programItem) ? Collections.EMPTY_SET : super._getStepsToCache(programItem, yearId, cache);
095    }
096    
097    private boolean _mustNotExport(ProgramItem programItem)
098    {
099        if (programItem instanceof Course || programItem instanceof Container)
100        {
101            return ((Content) programItem).getValue(OSEConstants.NO_OSE_EXPORT_ATTRIBUTE_NAME, false, false);
102        }
103        else
104        {
105            return false;
106        }
107    }
108    
109    /**
110     * Retrieve the {@link OrgUnit}s of a given {@link ProgramItem}
111     * @param programItem The program item
112     * @return the {@link OrgUnit}s of the given program item
113     */
114    public Set<OrgUnit> retrieveOrgUnits(ProgramItem programItem)
115    {
116        return _retrieveOrgUnits(programItem);
117    }
118    
119    private Set<OrgUnit> _retrieveOrgUnits(ProgramItem parent)
120    {
121        return Optional.of(parent)
122                .map(this::_getDirectOrgUnit)
123                .map(Collections::singleton)
124                .orElseGet(() -> _retrieveOrgUnitsFromParents(parent));
125    }
126    
127    private Set<OrgUnit> _retrieveOrgUnitsFromParents(ProgramItem programItem)
128    {
129        return _odfHelper.getParentProgramItems(programItem)
130                .stream()
131                .map(this::_retrieveOrgUnits)
132                .flatMap(Set::stream)
133                .collect(Collectors.toSet());
134    }
135    
136    private OrgUnit _getDirectOrgUnit(ProgramItem programItem)
137    {
138        if (programItem instanceof Course)
139        {
140            List<String> orgUnitIds = ((Course) programItem).getOrgUnits();
141            return _resolveAllAndGetFirst(orgUnitIds, (Course) programItem);
142        }
143        else if (programItem instanceof AbstractProgram)
144        {
145            List<String> orgUnitIds = ((AbstractProgram) programItem).getOrgUnits();
146            return _resolveAllAndGetFirst(orgUnitIds, (AbstractProgram) programItem);
147        }
148        else if (programItem instanceof Container)
149        {
150            return Optional.of(programItem)
151                    .map(Container.class::cast)
152                    .map(Container::getOrgUnit)
153                    .filter(Objects::nonNull)
154                    .map(this::_resolve)
155                    .filter(Objects::nonNull)
156                    .orElse(null);
157        }
158        return null;
159    }
160    
161    private <T extends ProgramItem & Content> OrgUnit _resolveAllAndGetFirst(Collection<String> orgUnitIds, T programElement)
162    {
163        List<OrgUnit> orgUnits = orgUnitIds.stream()
164                .map(this::_resolve)
165                .filter(Objects::nonNull)
166                .collect(Collectors.toList());
167        
168        if (orgUnits.size() > 1)
169        {
170            LogUtils.programElementWarningOrgUnits(getLogger(), programElement, orgUnits);
171        }
172        
173        return orgUnits.isEmpty() ? null : orgUnits.get(0);
174    }
175    
176    private OrgUnit _resolve(String id)
177    {
178        try
179        {
180            return _resolver.resolveById(id);
181        }
182        catch (AmetysRepositoryException e)
183        {
184            // Silently fail
185            return null;
186        }
187    }
188
189    /**
190     * Retrieve the degrees of a given {@link Container}
191     * @param container The container
192     * @return the degrees of the given container
193     */
194    public Set<OdfReferenceTableEntry> retrieveDegree(Container container)
195    {
196        return container.getRootPrograms()
197                .stream()
198                .map(p -> p.<ContentValue>getValue(AbstractProgram.DEGREE))
199                .filter(Objects::nonNull)
200                .map(ContentValue::getContentIfExists)
201                .filter(Optional::isPresent)
202                .map(Optional::get)
203                .map(OdfReferenceTableEntry::new)
204                .collect(Collectors.toSet());
205    }
206
207    /**
208     * 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.
209     * @param programItem The {@link ProgramItem} on which we have to retrieve the potential steps holder
210     * @return the potential steps holder of the program item
211     */
212    public Set<Container> retrieveStepsHolder(ProgramItem programItem)
213    {
214        Optional<String> yearId = getYearId();
215        if (yearId.isPresent())
216        {
217            return _getStepsHolder(programItem, yearId.get(), _cacheManager.get(_STEP_HOLDERS_BY_ITEM_CACHE_ID));
218        }
219        
220        return Set.of();
221    }
222    
223    /**
224     * 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.
225     * @param programItem The {@link ProgramItem} on which we have to retrieve the potential period types
226     * @return the potential period types of the program item
227     */
228    public Set<OdfReferenceTableEntry> retrievePeriodTypes(ProgramItem programItem)
229    {
230        return getSemesterId()
231            .map(id -> _retrievePeriodTypes(programItem, id))
232            .orElse(Collections.EMPTY_SET);
233    }
234
235    private Set<OdfReferenceTableEntry> _retrievePeriodTypes(ProgramItem programItem, String semesterId)
236    {
237        return _getPeriodType(programItem) // Try to get the period type on the current program item
238            .map(Collections::singleton)
239            .orElseGet(() -> _searchPeriodTypesInParents(programItem, semesterId)); // Or search in the parent program items
240    }
241    
242    private Set<OdfReferenceTableEntry> _searchPeriodTypesInParents(ProgramItem programItem, String semesterId)
243    {
244        if (_isContainerOfNature(programItem, semesterId))
245        {
246            return Collections.EMPTY_SET;
247        }
248        
249        // Only if programItem not a semester
250        return _odfHelper.getParentProgramItems(programItem) // Get the direct parent program items
251            .stream()
252            .map(parent -> _retrievePeriodTypes(parent, semesterId)) // Retrieve the period types for all parents
253            .flatMap(Set::stream)
254            .collect(Collectors.toSet());
255    }
256    
257    private Optional<OdfReferenceTableEntry> _getPeriodType(ProgramItem programItem)
258    {
259        return Optional.of(programItem)
260            .map(Content.class::cast) // Cast to Content (ProgramItem is always a Content)
261            .filter(c -> ModelHelper.hasModelItem("period", c.getModel())) // Filter if the attribute "period" is not in the model
262            .map(c -> c.<ContentValue>getValue("period")) // Get the period
263            .flatMap(ContentValue::getContentIfExists)
264            .map(c -> c.<ContentValue>getValue("type")) // Get the period type
265            .flatMap(ContentValue::getContentIfExists)
266            .map(OdfReferenceTableEntry::new);
267    }
268
269    
270    /**
271     * Get the semester container nature identifier.
272     * @return an {@link Optional} of the semester identifier
273     */
274    public Optional<String> getSemesterId()
275    {
276        return Optional.of(_refTableHelper)
277                .map(rth -> rth.getItemFromCode(OdfReferenceTableHelper.CONTAINER_NATURE, "semestre"))
278                .map(OdfReferenceTableEntry::getId)
279                .filter(StringUtils::isNotBlank);
280    }
281}