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