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