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.course;
017
018import java.time.LocalDate;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.configuration.Configurable;
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.collections4.CollectionUtils;
037import org.apache.commons.lang3.StringUtils;
038
039import org.ametys.cms.data.ContentDataHelper;
040import org.ametys.cms.repository.ContentQueryHelper;
041import org.ametys.cms.repository.ContentTypeExpression;
042import org.ametys.core.user.UserIdentity;
043import org.ametys.odf.ODFHelper;
044import org.ametys.odf.course.ShareableCourseStatusHelper.ShareableStatus;
045import org.ametys.odf.courselist.CourseList;
046import org.ametys.odf.enumeration.OdfReferenceTableEntry;
047import org.ametys.odf.enumeration.OdfReferenceTableHelper;
048import org.ametys.odf.program.Container;
049import org.ametys.odf.program.Program;
050import org.ametys.plugins.repository.AmetysObjectIterable;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.repository.query.expression.AndExpression;
053import org.ametys.plugins.repository.query.expression.Expression;
054import org.ametys.plugins.repository.query.expression.Expression.Operator;
055import org.ametys.plugins.repository.query.expression.StringExpression;
056import org.ametys.runtime.config.Config;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059/**
060 * Helper for shareable course
061 */
062public class ShareableCourseHelper extends AbstractLogEnabled implements Component, Serviceable, Configurable
063{
064    /** The component role. */
065    public static final String ROLE = ShareableCourseHelper.class.getName();
066
067    private static final String __OWN_KEY = "OWN";
068    
069    /** The shareable course configuration */
070    protected ShareableConfiguration _shareableCourseConfiguration;
071    
072    /** The ODF reference table helper */
073    protected OdfReferenceTableHelper _odfRefTableHelper;
074    
075    /** The shareable course status */
076    protected ShareableCourseStatusHelper _shareableCourseStatus;
077    
078    /** The ODF helper */
079    protected ODFHelper _odfHelper;
080    
081    /** The Ametys object resolver */
082    protected AmetysObjectResolver _resolver;
083    
084    public void service(ServiceManager manager) throws ServiceException
085    {
086        _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
087        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
088        _shareableCourseStatus = (ShareableCourseStatusHelper) manager.lookup(ShareableCourseStatusHelper.ROLE);
089        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
090    }
091    
092    public void configure(Configuration configuration) throws ConfigurationException
093    {
094        if ("shareable-fields".equals(configuration.getName()))
095        {
096            _shareableCourseConfiguration = _parseConfiguration(configuration);
097        }
098        else
099        {
100            _shareableCourseConfiguration = _parseConfiguration(configuration.getChild("shareable-fields"));
101        }
102        
103    }
104    
105    /**
106     * Parse configuration to create a shareable course configuration
107     * @param configuration the configuration
108     * @return the shareable course configuration
109     */
110    protected ShareableConfiguration _parseConfiguration(Configuration configuration)
111    {
112        boolean autoValidated = _parseAutoValidated(configuration);
113        
114        ShareableField programField = _parseProgramField(configuration);
115        ShareableField degreeField = _parseDegreeField(configuration);
116        ShareableField periodField = _parsePeriodField(configuration);
117        ShareableField orgUnitField = _parseOrgUnitField(configuration);
118        
119        return new ShareableConfiguration(programField, degreeField, orgUnitField, periodField, autoValidated);
120    }
121
122    
123    /**
124     * Parse configuration to know if courses are validated after initialization 
125     * @param configuration the configuration
126     * @return <code>true</code> if courses are validated after initialization 
127     */
128    protected boolean _parseAutoValidated(Configuration configuration)
129    {
130        return Boolean.valueOf(configuration.getAttribute("auto-validated", "false"));
131    }
132    
133    /**
134     * Parse configuration to get program field 
135     * @param configuration the configuration
136     * @return the program field
137     */
138    protected ShareableField _parseProgramField(Configuration configuration)
139    {
140        Configuration programsConf = configuration.getChild(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME);
141        String programValuesAsString = programsConf.getValue(StringUtils.EMPTY);
142        if (__OWN_KEY.equals(programValuesAsString))
143        {
144            return new ShareableField(true, Set.of());
145        }
146        else
147        {
148            List<String> valuesAsList = Arrays.asList(StringUtils.split(programValuesAsString, ","));
149            Set<String> programValues = valuesAsList.stream()
150                .map(StringUtils::trim)
151                .filter(StringUtils::isNotBlank)
152                .filter(this::_isContentExist)
153                .collect(Collectors.toSet());
154            
155            return new ShareableField(false, programValues);
156        }
157    }
158    
159    /**
160     * Parse configuration to get degree field 
161     * @param configuration the configuration
162     * @return the degree field
163     */
164    protected ShareableField _parseDegreeField(Configuration configuration)
165    {
166        Configuration degreesConf = configuration.getChild(ShareableCourseConstants.DEGREES_FIELD_ATTRIBUTE_NAME);
167        String degreeValuesAsString = degreesConf.getValue(StringUtils.EMPTY);
168        if (__OWN_KEY.equals(degreeValuesAsString))
169        {
170            return new ShareableField(true, Set.of());
171        }
172        else
173        {
174            List<String> valuesAsList = Arrays.asList(StringUtils.split(degreeValuesAsString, ","));
175            Set<String> degreeValues = valuesAsList.stream()
176                .map(StringUtils::trim)
177                .filter(StringUtils::isNotBlank)
178                .map(d -> _getItemIdFromCDM(d, OdfReferenceTableHelper.DEGREE))
179                .filter(Objects::nonNull)
180                .collect(Collectors.toSet());
181            
182            return new ShareableField(false, degreeValues);
183        }
184    }
185    
186    /**
187     * Parse configuration to get period field 
188     * @param configuration the configuration
189     * @return the period field
190     */
191    protected ShareableField _parsePeriodField(Configuration configuration)
192    {
193        Configuration periodsConf = configuration.getChild(ShareableCourseConstants.PERIODS_FIELD_ATTRIBUTE_NAME);
194        String periodValuesAsString = periodsConf.getValue(StringUtils.EMPTY);
195        if (__OWN_KEY.equals(periodValuesAsString))
196        {
197            return new ShareableField(true, Set.of());
198        }
199        else
200        {
201            List<String> valuesAsList = Arrays.asList(StringUtils.split(periodValuesAsString, ","));
202            Set<String> periodValues = valuesAsList.stream()
203                .map(StringUtils::trim)
204                .filter(StringUtils::isNotBlank)
205                .map(p -> _getItemIdFromCDM(p, OdfReferenceTableHelper.PERIOD))
206                .filter(Objects::nonNull)
207                .collect(Collectors.toSet());
208            
209            return new ShareableField(false, periodValues);
210        }
211    }
212    
213    /**
214     * Parse configuration to get orgUnit field 
215     * @param configuration the configuration
216     * @return the orgUnit field
217     */
218    protected ShareableField _parseOrgUnitField(Configuration configuration)
219    {
220        Configuration orgUnitsConf = configuration.getChild(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME);
221        String orgUnitValuesAsString = orgUnitsConf.getValue(StringUtils.EMPTY);
222        if (__OWN_KEY.equals(orgUnitValuesAsString))
223        {
224            return new ShareableField(true, Set.of());
225        }
226        else
227        {
228            List<String> valuesAsList = Arrays.asList(StringUtils.split(orgUnitValuesAsString, ","));
229            Set<String> orgUnitValues = valuesAsList.stream()
230                    .map(StringUtils::trim)
231                    .filter(StringUtils::isNotBlank)
232                    .filter(this::_isContentExist)
233                    .collect(Collectors.toSet());
234            
235            return new ShareableField(false, orgUnitValues);
236        }
237    }
238    
239    private String _getItemIdFromCDM(String cdmValue, String contentType)
240    {
241        OdfReferenceTableEntry content = _odfRefTableHelper.getItemFromCDM(contentType, cdmValue);
242        if (content == null)
243        {
244            getLogger().warn("Find a wrong data in the shareable configuration : can't get content from cdmValue {}", cdmValue);
245            return null;
246        }
247        
248        return content.getId();
249    }
250    
251    private boolean _isContentExist(String contentId)
252    {
253        try
254        {
255            _resolver.resolveById(contentId);
256            return true;
257        }
258        catch (Exception e) 
259        {
260            getLogger().warn("Find a wrong data in the shareable configuration : can't get content from id {}", contentId);
261            return false;
262        }
263    }
264    
265    /**
266     * Initialize the shareable course fields
267     * @param courseContent the course content to initialize
268     * @param courseListContent the course list parent. Can be null
269     * @param user the user who initialize the shareable fields
270     * @param ignoreRights true to ignore user rights
271     * @return <code>true</code> if there are changes
272     */
273    public boolean initializeShareableFields(Course courseContent, CourseList courseListContent, UserIdentity user, boolean ignoreRights)
274    {
275        List<CourseList> courseListContents = courseListContent != null ? Collections.singletonList(courseListContent) : List.of();
276        return initializeShareableFields(courseContent, courseListContents, user, ignoreRights);
277    }
278    
279    /**
280     * Initialize the shareable course fields
281     * @param courseContent the course content to initialize
282     * @param courseListContents the list of course list parents. Can be empty
283     * @param user the user who initialize the shareable fields
284     * @param ignoreRights true to ignore user rights
285     * @return <code>true</code> if there are changes
286     */
287    public boolean initializeShareableFields(Course courseContent, List<CourseList> courseListContents, UserIdentity user, boolean ignoreRights)
288    {
289        boolean hasChanges = false;
290        if (handleShareableCourse())
291        {
292            hasChanges = true;
293            
294            Set<Program> parentPrograms = new HashSet<>();
295            Set<Container> parentContainers = new HashSet<>();
296            for (CourseList courseList : courseListContents)
297            {
298                parentPrograms.addAll(_odfHelper.getParentPrograms(courseList));
299                parentContainers.addAll(_odfHelper.getParentContainers(courseList));
300            }
301            
302            ShareableField programField = _shareableCourseConfiguration.getProgramField();
303            Set<String> programs = programField.ownContext() ? getProgramIds(parentPrograms) : programField.getDefaultValues();
304            if (!programs.isEmpty())
305            {
306                courseContent.setValue(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME, programs.toArray(new String[programs.size()]));
307            }
308            
309            ShareableField degreeField = _shareableCourseConfiguration.getDegreeField();
310            Set<String> degrees = degreeField.ownContext() ? getDegrees(parentPrograms) : degreeField.getDefaultValues();
311            if (!degrees.isEmpty())
312            {
313                courseContent.setValue(ShareableCourseConstants.DEGREES_FIELD_ATTRIBUTE_NAME, degrees.toArray(new String[degrees.size()]));
314            }
315            
316            ShareableField periodField = _shareableCourseConfiguration.getPeriodField();
317            Set<String> periods = periodField.ownContext() ? getPeriods(parentContainers) : periodField.getDefaultValues();
318            if (!periods.isEmpty())
319            {
320                courseContent.setValue(ShareableCourseConstants.PERIODS_FIELD_ATTRIBUTE_NAME, periods.toArray(new String[periods.size()]));
321            }
322            
323            ShareableField orgUnitField = _shareableCourseConfiguration.getOrgUnitField();
324            Set<String> orgUnits = orgUnitField.ownContext() ? getOrgUnits(parentPrograms) : orgUnitField.getDefaultValues();
325            if (!orgUnits.isEmpty())
326            {
327                courseContent.setValue(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME, orgUnits.toArray(new String[orgUnits.size()]));
328            }
329            
330            if (_shareableCourseConfiguration.autoValidated())
331            {
332                _shareableCourseStatus.setWorkflowStateAttribute(courseContent, LocalDate.now(), user, ShareableStatus.VALIDATED, null, ignoreRights);
333            }
334        }
335        
336        return hasChanges;
337    }
338    
339    /**
340     * True if shareable course fields match with the course list
341     * @param courseContent the shareable course
342     * @param courseList the course list
343     * @return <code>true</code> if shareable course fields match with the course list
344     */
345    public boolean isShareableFieldsMatch(Course courseContent, CourseList courseList)
346    {
347        if (_shareableCourseStatus.getShareableStatus(courseContent) != ShareableStatus.VALIDATED)
348        {
349            return false;
350        }
351
352        List<String> programValues = ContentDataHelper.getContentIdsListFromMultipleContentData(courseContent, ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME);
353        List<String> degreeValues = ContentDataHelper.getContentIdsListFromMultipleContentData(courseContent, ShareableCourseConstants.DEGREES_FIELD_ATTRIBUTE_NAME);
354        List<String> orgUnitValues = ContentDataHelper.getContentIdsListFromMultipleContentData(courseContent, ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME);
355        List<String> periodValues = ContentDataHelper.getContentIdsListFromMultipleContentData(courseContent, ShareableCourseConstants.PERIODS_FIELD_ATTRIBUTE_NAME);
356
357        if (programValues.isEmpty() && degreeValues.isEmpty() && orgUnitValues.isEmpty() && periodValues.isEmpty())
358        {
359            return true;
360        }
361        
362        boolean isSharedWith = true;
363        if (!periodValues.isEmpty())
364        {
365            Set<Container> parentContainers = _odfHelper.getParentContainers(courseList);
366            Set<String> courseListPeriods = getPeriods(parentContainers);
367            
368            isSharedWith = CollectionUtils.containsAny(courseListPeriods, periodValues) && isSharedWith;
369        }
370        
371        if (isSharedWith)
372        {
373            Set<Program> parentPrograms = _odfHelper.getParentPrograms(courseList);
374
375            if (!programValues.isEmpty())
376            {
377                Set<String> courseListprogramIds = getProgramIds(parentPrograms);
378                isSharedWith = CollectionUtils.containsAny(courseListprogramIds, programValues);
379            }
380            
381            if (!degreeValues.isEmpty())
382            {
383                Set<String> courseListDegrees = getDegrees(parentPrograms);
384                isSharedWith = isSharedWith && CollectionUtils.containsAny(courseListDegrees, degreeValues);
385            }
386            
387            if (!orgUnitValues.isEmpty())
388            {
389                Set<String> courseListorgUnitIds = getOrgUnits(parentPrograms);
390                isSharedWith = isSharedWith && CollectionUtils.containsAny(courseListorgUnitIds, orgUnitValues);
391            }
392        }
393        
394        return isSharedWith;
395    }
396    
397    /**
398     * Get programs ids from programs
399     * @param programs the list of programs
400     * @return the list of progam ids
401     */
402    public Set<String> getProgramIds(Set<Program> programs)
403    {
404        return programs.stream()
405                .map(Program::getId)
406                .collect(Collectors.toSet());
407    }
408    
409    /** 
410     * Get degrees from programs
411     * @param programs the list of programs
412     * @return the list of degrees
413     */
414    public Set<String> getDegrees(Set<Program> programs)
415    {
416        return programs.stream()
417                .map(Program::getDegree)
418                .filter(StringUtils::isNotBlank)
419                .collect(Collectors.toSet());
420    }
421    
422    /** 
423     * Get periods from containers
424     * @param containers the list of containers
425     * @return the list of periods
426     */
427    public Set<String> getPeriods(Set<Container> containers)
428    {
429        return containers.stream()
430                .map(Container::getPeriod)
431                .filter(StringUtils::isNotBlank)
432                .collect(Collectors.toSet());
433    }
434    
435    /** 
436     * Get orgUnits from programs
437     * @param programs the list of programs
438     * @return the list of orgUnits
439     */
440    public Set<String> getOrgUnits(Set<Program> programs)
441    {
442        return programs.stream()
443                .map(Program::getOrgUnits)
444                .flatMap(Collection::stream)
445                .filter(StringUtils::isNotBlank)
446                .collect(Collectors.toSet());
447    }
448    
449    /**
450     * Return true if shareable course are handled
451     * @return <code>true</code> if shareable course are handled
452     */
453    public boolean handleShareableCourse()
454    {
455        boolean handleShareableCourse = Config.getInstance().getValue("odf.shareable.course.enable");
456        return handleShareableCourse;
457    }
458    
459    /**
460     * Get the {@link Course}s with shareable filters matching the given arguments
461     * @param programId the program id. Can be null
462     * @param degreeId the degree id. Can be null
463     * @param periodId the period id. Can be null
464     * @param orgUnitId the orgUnit id. Can be null
465     * @return The matching courses
466     */
467    public AmetysObjectIterable<Course> getShareableCourses(String programId, String degreeId, String periodId, String orgUnitId)
468    {
469        List<Expression> exprs = new ArrayList<>();
470        exprs.add(new ContentTypeExpression(Operator.EQ, CourseFactory.COURSE_CONTENT_TYPE));
471        
472        if (StringUtils.isNotEmpty(programId))
473        {
474            exprs.add(new StringExpression(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME, Operator.EQ, programId));
475        }
476        
477        if (StringUtils.isNotEmpty(degreeId))
478        {
479            exprs.add(new StringExpression(ShareableCourseConstants.DEGREES_FIELD_ATTRIBUTE_NAME, Operator.EQ, degreeId));
480        }
481        
482        if (StringUtils.isNotEmpty(periodId))
483        {
484            exprs.add(new StringExpression(ShareableCourseConstants.PERIODS_FIELD_ATTRIBUTE_NAME, Operator.EQ, periodId));
485        }
486        
487        if (StringUtils.isNotEmpty(orgUnitId))
488        {
489            exprs.add(new StringExpression(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME, Operator.EQ, orgUnitId));
490        }
491        
492        Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
493        
494        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr);
495        return _resolver.query(xpathQuery);
496    } 
497    
498    
499    private static class ShareableField
500    {
501        private boolean _own;
502        private Set<String> _values;
503        
504        public ShareableField(boolean own, Set<String> values)
505        {
506            _own = own;
507            _values = values;
508        }
509        
510        public boolean ownContext()
511        {
512            return _own;
513        }
514        
515        public Set<String> getDefaultValues()
516        {
517            return _values;
518        }
519    }
520    
521    private static class ShareableConfiguration
522    {
523        private ShareableField _programField;
524        private ShareableField _degreeField;
525        private ShareableField _orgUnitField;
526        private ShareableField _periodField;
527        private boolean _autoValidated;
528        
529        public ShareableConfiguration(ShareableField programField, ShareableField degreeField, ShareableField orgUnitField, ShareableField periodField, boolean autoValidated)
530        {
531            _programField = programField;
532            _degreeField = degreeField;
533            _orgUnitField = orgUnitField;
534            _periodField = periodField;
535            _autoValidated = autoValidated;
536        }
537        
538        public boolean autoValidated()
539        {
540            return _autoValidated;
541        }
542        
543        public ShareableField getProgramField()
544        {
545            return _programField;
546        }
547        
548        public ShareableField getDegreeField()
549        {
550            return _degreeField;
551        }
552        
553        public ShareableField getOrgUnitField()
554        {
555            return _orgUnitField;
556        }
557        
558        public ShareableField getPeriodField()
559        {
560            return _periodField;
561        }
562    }
563    
564}