001/*
002 *  Copyright 2018 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;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import javax.jcr.RepositoryException;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.ArrayUtils;
033import org.apache.commons.lang3.tuple.Pair;
034
035import org.ametys.cms.FilterNameHelper;
036import org.ametys.cms.content.external.ExternalizableMetadataHelper;
037import org.ametys.cms.repository.ContentQueryHelper;
038import org.ametys.cms.repository.ContentTypeExpression;
039import org.ametys.cms.repository.ModifiableDefaultContent;
040import org.ametys.cms.repository.WorkflowAwareContent;
041import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
042import org.ametys.cms.workflow.ContentWorkflowHelper;
043import org.ametys.cms.workflow.CreateContentFunction;
044import org.ametys.odf.course.Course;
045import org.ametys.odf.course.CourseFactory;
046import org.ametys.odf.coursepart.CoursePart;
047import org.ametys.odf.coursepart.CoursePartFactory;
048import org.ametys.odf.enumeration.OdfReferenceTableEntry;
049import org.ametys.odf.enumeration.OdfReferenceTableHelper;
050import org.ametys.odf.workflow.ValidateODFContentFunction;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.AmetysRepositoryException;
054import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
055import org.ametys.plugins.repository.query.expression.AndExpression;
056import org.ametys.plugins.repository.query.expression.Expression;
057import org.ametys.plugins.repository.query.expression.Expression.Operator;
058import org.ametys.plugins.repository.query.expression.MetadataExpression;
059import org.ametys.plugins.repository.query.expression.OrExpression;
060import org.ametys.plugins.workflow.AbstractWorkflowComponent;
061import org.ametys.plugins.workflow.support.WorkflowProvider;
062import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064
065import com.opensymphony.workflow.InvalidActionException;
066import com.opensymphony.workflow.WorkflowException;
067
068/**
069 * Initialization class to migrate totalDurationOf* metadata.
070 */
071public class MigrateCoursePart extends AbstractLogEnabled implements org.ametys.runtime.plugin.Init, Serviceable
072{
073    /** The Ametys object resolver */
074    protected AmetysObjectResolver _resolver;
075    /** The ODF Reference table helper */
076    protected OdfReferenceTableHelper _odfRefTableHelper;
077    /** The content workflow helper */
078    protected ContentWorkflowHelper _workflowHelper;
079    /** The workflow */
080    protected WorkflowProvider _workflowProvider;
081
082    @Override
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
086        _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
087        _workflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
088        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
089    }
090
091    @Override
092    public void init() throws Exception
093    {
094        _migrateCourseParts();
095    }
096    
097    /**
098     * Migrate the totalDurationOf to course parts.
099     * @throws AmetysRepositoryException if an error occurs
100     * @throws WorkflowException if an error occurs
101     * @throws RepositoryException if an error occurs
102     */
103    protected void _migrateCourseParts() throws AmetysRepositoryException, WorkflowException, RepositoryException
104    {
105        Map<String, OdfReferenceTableEntry> natureByCode = _createNaturesEnseignement();
106        
107        // Migrate existing totalDurationOf*
108        Expression[] metadataExpressions =  natureByCode.keySet()
109            .stream()
110            .map(code -> new MetadataExpression("totalDurationOf" + code))
111            .toArray(MetadataExpression[]::new);
112        
113        String xpathQuery = ContentQueryHelper.getContentXPathQuery(
114            new AndExpression(
115                new ContentTypeExpression(Operator.EQ, CourseFactory.COURSE_CONTENT_TYPE),
116                new OrExpression(metadataExpressions)
117            ));
118        
119        AmetysObjectIterable<Course> courses = _resolver.query(xpathQuery);
120        for (Course course : courses)
121        {
122            boolean hasLiveVersion = ArrayUtils.contains(course.getAllLabels(), ValidateODFContentFunction.VALID_LABEL);
123            boolean currentVersionIsLive = hasLiveVersion && ArrayUtils.contains(course.getLabels(), ValidateODFContentFunction.VALID_LABEL);
124            
125            if (hasLiveVersion && !currentVersionIsLive)
126            {
127                String currentVersion = course.getNode()
128                    .getSession()
129                    .getWorkspace()
130                    .getVersionManager()
131                    .getBaseVersion(course.getNode().getPath())
132                    .getName();
133                
134                // switching to old live version
135                course.restoreFromLabel(ValidateODFContentFunction.VALID_LABEL);
136                
137                // migrate old live version
138                Map<String, CoursePart> createdCourseParts = _migrateCoursePart(course, natureByCode, true);
139                
140                // restore current version
141                course.restoreFromRevision(currentVersion);
142                
143                // update current version
144                _updateCoursePart(course, natureByCode, createdCourseParts);
145            }
146            else
147            {
148                // migrate current version
149                _migrateCoursePart(course, natureByCode, currentVersionIsLive);
150            }
151        }
152    }
153    
154    /**
155     * Update the {@link CoursePart} and the course parts list of the {@link Course} with the values of the current version.
156     * @param course The {@link Course} to migrate
157     * @param natureByCode The course part natures
158     * @param createdCourseParts The {@link Map} of the created {@link CoursePart} previously
159     */
160    protected void _updateCoursePart(Course course, Map<String, OdfReferenceTableEntry> natureByCode, Map<String, CoursePart> createdCourseParts)
161    {
162        ModifiableCompositeMetadata cm = course.getMetadataHolder();
163        
164        Set<String> courseParts = new HashSet<>();
165        
166        for (String natureCode : natureByCode.keySet())
167        {
168            String metadataName = "totalDurationOf" + natureCode;
169            double totalDurationOf = cm.getDouble(metadataName, 0);
170            if (totalDurationOf > 0)
171            {
172                CoursePart coursePart = createdCourseParts.get(natureCode);
173                if (coursePart == null)
174                {
175                    // Create course part
176                    Optional.ofNullable(_createCoursePart(course, natureByCode.get(natureCode), totalDurationOf, false))
177                        .ifPresent(createdCoursePart -> courseParts.add(createdCoursePart.getId()));
178                }
179                else
180                {
181                    // Update course part
182                    double oldValue = coursePart.getMetadataHolder().getDouble(CoursePart.METADATA_NB_HOURS, 0);
183                    if (oldValue != totalDurationOf)
184                    {
185                        coursePart.getMetadataHolder().setMetadata(CoursePart.METADATA_NB_HOURS, totalDurationOf);
186                        coursePart.saveChanges();
187                        coursePart.checkpoint();
188                    }
189                    
190                    courseParts.add(coursePart.getId());
191                }
192            }
193            ExternalizableMetadataHelper.removeMetadataIfExists(cm, metadataName);
194        }
195        
196        // Add the course part to the course
197        if (!courseParts.isEmpty())
198        {
199            Collections.addAll(courseParts, cm.getStringArray(Course.METADATA_CHILD_COURSE_PARTS, new String[0]));
200            cm.setMetadata(Course.METADATA_CHILD_COURSE_PARTS, courseParts.toArray(new String[courseParts.size()]));
201        }
202        else
203        {
204            ExternalizableMetadataHelper.removeMetadataIfExists(cm, Course.METADATA_CHILD_COURSE_PARTS);
205        }
206        
207        if (course.needsSave())
208        {
209            course.saveChanges();
210            course.checkpoint();
211        }
212    }
213    
214    /**
215     * Create a course part linked to the course.
216     * @param course The {@link Course} to migrate
217     * @param natureByCode The course part natures
218     * @param isLive Set the Live label if <code>true</code>
219     * @return The created course parts
220     */
221    protected Map<String, CoursePart> _migrateCoursePart(Course course, Map<String, OdfReferenceTableEntry> natureByCode, boolean isLive)
222    {
223        ModifiableCompositeMetadata cm = course.getMetadataHolder();
224        
225        Map<String, CoursePart> createdCourseParts = new HashMap<>();
226        
227        // Create the course parts
228        for (String natureCode : natureByCode.keySet())
229        {
230            String metadataName = "totalDurationOf" + natureCode;
231            double totalDurationOf = cm.getDouble(metadataName, 0);
232            if (totalDurationOf > 0)
233            {
234                Optional.ofNullable(_createCoursePart(course, natureByCode.get(natureCode), totalDurationOf, isLive))
235                    .ifPresent(coursePart -> createdCourseParts.put(natureCode, coursePart));
236            }
237            ExternalizableMetadataHelper.removeMetadataIfExists(cm, metadataName);
238        }
239        
240        // Add the course part to the course
241        if (!createdCourseParts.isEmpty())
242        {
243            Set<String> coursePartIds = createdCourseParts.values()
244                .stream()
245                .map(CoursePart::getId)
246                .collect(Collectors.toSet());
247            Collections.addAll(coursePartIds, cm.getStringArray(Course.METADATA_CHILD_COURSE_PARTS, new String[0]));
248            cm.setMetadata(Course.METADATA_CHILD_COURSE_PARTS, coursePartIds.toArray(new String[coursePartIds.size()]));
249        }
250        
251        if (course.needsSave())
252        {
253            // Versions précédentes incompatibles
254            course.addLabel("NotCompatible", true);
255            
256            // Sauvegarde et avancement
257            course.saveChanges();
258            course.checkpoint();
259        }
260        
261        if (isLive)
262        {
263            course.addLabel(ValidateODFContentFunction.VALID_LABEL, true);
264        }
265        
266        return createdCourseParts;
267    }
268    
269    /**
270     * Create a course part linked to the course.
271     * @param course The {@link Course} holder
272     * @param nature The nature of the course part
273     * @param totalDurationOf The number of hours
274     * @param isLive Set the Live label if <code>true</code>
275     * @return The {@link CoursePart} id
276     */
277    protected CoursePart _createCoursePart(Course course, OdfReferenceTableEntry nature, Double totalDurationOf, boolean isLive)
278    {
279        // Create the course part
280        String coursePartTitle = course.getTitle();
281        if (nature != null)
282        {
283            coursePartTitle += " - " + nature.getCode();
284        }
285        String coursePartName = FilterNameHelper.filterName(coursePartTitle);
286        
287        Map<String, Object> resultMap = new HashMap<>();
288        
289        Map<String, Object> inputs = new HashMap<>();
290        inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, course.getLanguage());
291        inputs.put(CreateContentFunction.CONTENT_NAME_KEY, coursePartName); 
292        inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, coursePartTitle);
293        inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE});
294        
295        Map<String, Object> results = new HashMap<>();
296        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results);
297        
298        CoursePart coursePart = null;
299        try
300        {
301            Map<String, Object> workflowResult = _workflowHelper.createContent("course-part", 1, coursePartName, coursePartTitle, new String[] {CoursePartFactory.COURSE_PART_CONTENT_TYPE}, null, course.getLanguage());
302            coursePart = (CoursePart) workflowResult.get(AbstractContentWorkflowComponent.CONTENT_KEY);
303            
304            ModifiableCompositeMetadata coursePartCM = coursePart.getMetadataHolder();
305            if (nature != null)
306            {
307                coursePartCM.setMetadata(CoursePart.METADATA_NATURE, nature.getId());
308            }
309            coursePartCM.setMetadata(CoursePart.METADATA_NB_HOURS, totalDurationOf);
310            coursePartCM.setMetadata(CoursePart.METADATA_COURSE_HOLDER, course.getId());
311            coursePartCM.setMetadata(CoursePart.METADATA_PARENT_COURSES, new String[] {course.getId()});
312            coursePartCM.setMetadata(CoursePart.METADATA_CATALOG, course.getCatalog());
313            
314            coursePart.saveChanges();
315            coursePart.checkpoint();
316            
317            if (isLive)
318            {
319                coursePart.addLabel(ValidateODFContentFunction.VALID_LABEL, true);
320            }
321        }
322        catch (WorkflowException e)
323        {
324            resultMap.put("error", Boolean.TRUE);
325            getLogger().error("Failed to initialize workflow for content '{}' and language '{}'", coursePartTitle, course.getLanguage(), e);
326        }
327        
328        return coursePart;
329    }
330    
331    /**
332     * Create entries into the reference table EnseignementNature if they don't exist.
333     * @return The list of natures with their code and associated ID.
334     * @throws AmetysRepositoryException if an error occurs
335     * @throws WorkflowException if an error occurs
336     */
337    protected Map<String, OdfReferenceTableEntry> _createNaturesEnseignement() throws AmetysRepositoryException, WorkflowException
338    {
339        Map<String, String> categoryByCode = _createNatureEnseignementCategories();
340        Map<String, OdfReferenceTableEntry> natureByCode = new HashMap<>();
341        
342        for (Map.Entry<String, Pair<String, String>> nature : _getNaturesEnseignementList().entrySet())
343        {
344            String natureCode = nature.getKey();
345            OdfReferenceTableEntry natureEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE, natureCode);
346            
347            if (natureEntry == null)
348            {
349                String title = nature.getValue().getLeft();
350                String categoryCode = nature.getValue().getRight();
351                Map<String, String> titleVariants = new HashMap<>();
352                titleVariants.put("fr", title);
353                Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, titleVariants, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE}, new String[0]);
354                ModifiableDefaultContent natureContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
355                natureContent.getMetadataHolder().setMetadata("code", natureCode);
356                natureContent.getMetadataHolder().setMetadata("category", categoryByCode.get(categoryCode));
357                natureContent.saveChanges();
358                _doAction(natureContent, 22);
359                
360                natureEntry = new OdfReferenceTableEntry(natureContent);
361            }
362            
363            natureByCode.put(natureCode, natureEntry);
364        }
365        
366        return natureByCode;
367    }
368
369    /**
370     * Create entries into the reference table EnseignementNature if they don't exist.
371     * @return The list of natures with their code and associated ID.
372     * @throws AmetysRepositoryException if an error occurs
373     * @throws WorkflowException if an error occurs
374     */
375    protected Map<String, String> _createNatureEnseignementCategories() throws AmetysRepositoryException, WorkflowException
376    {
377        Map<String, String> categoryByCode = new HashMap<>();
378        
379        for (Map.Entry<String, Pair<String, Long>> category : _getNaturesEnseignementCategoryList().entrySet())
380        {
381            String categoryCode = category.getKey();
382            OdfReferenceTableEntry categoryEntry = _getOrCreateNatureEnseignement(category.getValue().getLeft(), categoryCode, category.getValue().getRight());
383            categoryByCode.put(categoryCode, categoryEntry.getId());
384        }
385        
386        return categoryByCode;
387    }
388    
389    /**
390     * List of the course parts natures.
391     * @return A {@link Map} with the code as a key, and a {@link Pair} with the title and the category code as a value
392     */
393    protected Map<String, Pair<String, String>> _getNaturesEnseignementList()
394    {
395        Map<String, Pair<String, String>> natures = new HashMap<>();
396        natures.put("CM", Pair.of("Cours Magistral", "CM"));
397        natures.put("TD", Pair.of("Travaux Dirigés", "TD"));
398        natures.put("TP", Pair.of("Travaux Pratique", "TP"));
399        return natures;
400    }
401    
402    /**
403     * List of the course parts nature categories.
404     * @return A {@link Map} with the code as a key, and the title as a value
405     */
406    protected Map<String, Pair<String, Long>> _getNaturesEnseignementCategoryList()
407    {
408        Map<String, Pair<String, Long>> categories = new HashMap<>();
409        categories.put("CM", Pair.of("Cours Magistral", 1L));
410        categories.put("TD", Pair.of("Travaux Dirigés", 2L));
411        categories.put("TP", Pair.of("Travaux Pratique", 3L));
412        return categories;
413    }
414    
415    /**
416     * {@link ContentWorkflowHelper} cannot be used in these conditions.
417     * @param content The content
418     * @param actionId Action to perform
419     * @return The result map
420     * @throws WorkflowException if an error occurs
421     * @throws AmetysRepositoryException if an error occurs
422     */
423    protected Map<String, Object> _doAction(WorkflowAwareContent content, Integer actionId) throws AmetysRepositoryException, WorkflowException
424    {
425        Map<String, Object> inputs = new HashMap<>();
426        Map<String, Object> results = new HashMap<>();
427        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results);
428        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
429        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
430        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, new HashMap<String, Object>());
431        
432        try
433        {
434            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
435            workflow.doAction(content.getWorkflowId(), actionId, inputs);
436        }
437        catch (InvalidActionException e)
438        {
439            getLogger().error("An error occured while do workflow action '{}' on content '{}'", actionId, content.getId(), e);
440            throw e; 
441        }
442        
443        return results;
444    }
445    
446    /**
447     * Get or create the nature enseignement if it doesn't exist. The code is tested.
448     * @param title The title of the nature
449     * @param code The code of the nature
450     * @param order The order to set.
451     * @return The corresponding entry
452     * @throws AmetysRepositoryException if an error occurs
453     * @throws WorkflowException if an error occurs
454     */
455    protected OdfReferenceTableEntry _getOrCreateNatureEnseignement(String title, String code, Long order) throws AmetysRepositoryException, WorkflowException
456    {
457        OdfReferenceTableEntry categoryEntry = _odfRefTableHelper.getItemFromCode(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY, code);
458        
459        if (categoryEntry == null)
460        {
461            Map<String, Object> result = _workflowHelper.createContent("reference-table", 1, title, title, new String[] {OdfReferenceTableHelper.ENSEIGNEMENT_NATURE_CATEGORY}, new String[0], "fr");
462            ModifiableDefaultContent categoryContent = (ModifiableDefaultContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
463            categoryContent.getMetadataHolder().setMetadata("code", code);
464            categoryContent.getMetadataHolder().setMetadata("order", order);
465            categoryContent.saveChanges();
466            _doAction(categoryContent, 22);
467            
468            categoryEntry = new OdfReferenceTableEntry(categoryContent);
469        }
470        
471        return categoryEntry;
472    }
473}