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.plugins.odfpilotage.report.impl;
017
018import java.io.IOException;
019import java.util.Arrays;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024import java.util.Set;
025import java.util.TreeSet;
026
027import javax.xml.transform.sax.TransformerHandler;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.xml.sax.SAXException;
035
036import org.ametys.cms.content.compare.ContentComparator;
037import org.ametys.cms.content.compare.ContentComparatorChange;
038import org.ametys.cms.content.compare.ContentComparatorResult;
039import org.ametys.cms.data.ContentDataHelper;
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.repository.Content;
042import org.ametys.odf.ProgramItem;
043import org.ametys.odf.course.Course;
044import org.ametys.odf.program.Program;
045import org.ametys.plugins.odfpilotage.report.impl.mcc.MCCAmetysObjectTree;
046import org.ametys.plugins.odfpilotage.schedulable.AbstractReportSchedulable;
047import org.ametys.plugins.odfpilotage.schedulable.OrgUnitMCCDiffReportSchedulable;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
050import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
051import org.ametys.runtime.model.ModelItem;
052
053/**
054 * The compare MCC catalog report (based on MCC report).
055 */
056public class MCCDiffReport extends AbstractMCCReport
057{
058    /** The key for the old catalog */
059    public static final String PARAMETER_CATALOG_OLD = "catalogOld";
060    
061    private String _oldCatalog;
062    private ContentComparator _contentComparator;
063
064    /**
065     * Change type
066     */
067    protected enum ChangeType
068    {
069        /**
070         * The metadata is added in the new content
071         */
072        ADDED,
073        /**
074         * The metadata is removed in the new content
075         */
076        REMOVED,
077        /**
078         * The metadata is modified in the old content
079         */
080        MODIFIED_OLD,
081        /**
082         * The metadata is modified in the new content
083         */
084        MODIFIED_NEW
085    }
086    
087    @Override
088    protected String getType()
089    {
090        return "mccdiff";
091    }
092    
093    @Override
094    public boolean isGeneric()
095    {
096        return false;
097    }
098
099    @Override
100    protected boolean isCompatibleSchedulable(AbstractReportSchedulable schedulable)
101    {
102        return schedulable instanceof OrgUnitMCCDiffReportSchedulable;
103    }
104    
105    @Override
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        super.service(manager);
109        _contentComparator = (ContentComparator) manager.lookup(ContentComparator.ROLE);
110    }
111
112    @Override
113    protected String launchByOrgUnit(Map<String, String> reportParameters) throws Exception
114    {
115        _oldCatalog = reportParameters.get(PARAMETER_CATALOG_OLD);
116        if (_oldCatalog.equals(reportParameters.get(PARAMETER_CATALOG)))
117        {
118            getLogger().error("Le catalogue de comparaison ne peut pas avoir la même valeur que le catalogue de référence.");
119            return null;
120        }
121        return super.launchByOrgUnit(reportParameters);
122    }
123    
124    @Override
125    protected void addProgram2MCCAmetysObjectTree(MCCAmetysObjectTree tree, Program program)
126    {
127        Program oldProgram = _findOldProgramItem(program);
128        if (oldProgram != null)
129        {
130            ContentComparatorResult changes = _getObjectChanges(oldProgram, program);
131            MCCAmetysObjectTree programTree = tree.addChild(program, changes);
132            populateMCCAmetysObjectTree(programTree);
133        }
134    }
135    
136    @Override
137    protected void populateMCCAmetysObjectTree(MCCAmetysObjectTree tree)
138    {
139        ProgramItem oldProgramItem = (ProgramItem) tree.getChange().getContent1();
140        ProgramItem newProgramItem = (ProgramItem) tree.getChange().getContent2();
141        
142        if (oldProgramItem == null)
143        {
144            // Only new
145            List<ProgramItem> children = _odfHelper.getChildProgramItems(newProgramItem);
146            for (ProgramItem child : children)
147            {
148                ContentComparatorResult changes = _getObjectChanges(null, child);
149                MCCAmetysObjectTree childTree = tree.addChild(child, changes);
150                populateMCCAmetysObjectTree(childTree);
151            }
152        }
153        else if (newProgramItem == null)
154        {
155            // Only old
156            List<ProgramItem> children = _odfHelper.getChildProgramItems(oldProgramItem);
157            for (ProgramItem child : children)
158            {
159                ContentComparatorResult changes = _getObjectChanges(child, null);
160                MCCAmetysObjectTree childTree = tree.addChild(child, changes);
161                populateMCCAmetysObjectTree(childTree);
162            }
163        }
164        else
165        {
166            List<ProgramItem> oldChildren = _odfHelper.getChildProgramItems(oldProgramItem);
167            List<ProgramItem> newChildren = _odfHelper.getChildProgramItems(newProgramItem);
168            
169            // Add old program items
170            for (ProgramItem oldChild : oldChildren)
171            {
172                ProgramItem currentNewChild = null;
173                Class<? extends ProgramItem> oldChildClass = oldChild.getClass();
174                String oldChildCode = oldChild.getCode();
175                for (ProgramItem newChild : newChildren)
176                {
177                    if (newChild.getCode().equals(oldChildCode) && newChild.getClass().equals(oldChildClass))
178                    {
179                        currentNewChild = newChild;
180                        // Decrease the size of new children to do the next operations faster
181                        newChildren.remove(newChild);
182                        break;
183                    }
184                }
185                
186                ContentComparatorResult changes = _getObjectChanges(oldChild, currentNewChild);
187                MCCAmetysObjectTree childTree = tree.addChild(oldChild, changes);
188                populateMCCAmetysObjectTree(childTree);
189            }
190            
191            // Add new program items
192            for (ProgramItem newChild : newChildren)
193            {
194                ContentComparatorResult changes = _getObjectChanges(null, newChild);
195                MCCAmetysObjectTree childTree = tree.addChild(newChild, changes);
196                populateMCCAmetysObjectTree(childTree);
197            }
198        }
199    }
200
201    private ContentComparatorResult _getObjectChanges(ProgramItem oldProgramItem, ProgramItem newProgramItem)
202    {
203        return (oldProgramItem instanceof Course || newProgramItem instanceof Course)
204                ? _getCourseChanges((Course) oldProgramItem, (Course) newProgramItem)
205                : new ContentComparatorResult((Content) oldProgramItem, (Content) newProgramItem);
206    }
207    
208    /**
209     * Compare two courses.
210     * @param oldCourse The first course to compare (the old one)
211     * @param newCourse The second course to compare (the new one)
212     * @return A {@link ContentComparatorResult}, or null if an exception occured
213     */
214    private ContentComparatorResult _getCourseChanges(Course oldCourse, Course newCourse)
215    {
216        if (oldCourse == null || newCourse == null)
217        {
218            return new ContentComparatorResult(oldCourse, newCourse);
219        }
220
221        ContentComparatorResult changes = null;
222        try
223        {
224            changes = _contentComparator.compare(oldCourse, newCourse, "mcc");
225            if (!changes.areEquals())
226            {
227                getLogger().debug("Différence trouvée pour l'ELP {} - {}", oldCourse.getCode(), oldCourse.getTitle());
228            }
229        }
230        catch (AmetysRepositoryException | IOException e)
231        {
232            getLogger().error("Une erreur est survenue pour l'ELP {} - {}", oldCourse.getCode(), oldCourse.getTitle(), e);
233        }
234        
235        return changes;
236    }
237    
238    /**
239     * Find the equivalent content in the new catalog
240     * @param <T> Type of content to find
241     * @param content Content in the current catalog
242     * @return New equivalent content or null
243     */
244    private <T extends ProgramItem> T _findOldProgramItem(T content)
245    {
246        return _odfHelper.getProgramItem(content, _oldCatalog, content.getLanguage());
247    }
248    
249    private Set<Integer> _getEntryPositions(Content content, String sessionName)
250    {
251        Set<Integer> entryPositions = new HashSet<>();
252        if (content != null)
253        {
254            if (content.hasValue(sessionName))
255            {
256                content.getRepeater(sessionName)
257                       .getEntries()
258                       .stream()
259                       .map(RepeaterEntry::getPosition)
260                       .forEach(entryPositions::add);
261            }
262        }
263        return entryPositions;
264    }
265
266    @Override
267    protected void saxMCCs(TransformerHandler handler, Course course, MCCAmetysObjectTree tree) throws SAXException
268    {
269        ContentComparatorResult changes = tree.getChange();
270        
271        XMLUtils.startElement(handler, "mcc");
272
273        // sax MCC
274        List<String> sessionNames = Arrays.asList("mccSession1", "mccSession2");
275            
276        // session 1 + session 2
277        for (String sessionName : sessionNames)
278        {
279            // entryPositions1 = entry positions du content 1 (ancien)
280            // entryPositions2 = entry positions du content 2 (nouveau)
281            // entryPositions = concaténation des deux ordonnée
282            Set<Integer> entryPositions1 = _getEntryPositions(changes.getContent1(), sessionName);
283            Set<Integer> entryPositions2 = _getEntryPositions(changes.getContent2(), sessionName);
284            Set<Integer> allEntryPositions = new TreeSet<>();
285            allEntryPositions.addAll(entryPositions1);
286            allEntryPositions.addAll(entryPositions2);
287            
288            if (!allEntryPositions.isEmpty())
289            {
290                AttributesImpl sessionAttrs = new AttributesImpl();
291                sessionAttrs.addCDATAAttribute("num", String.valueOf(sessionNames.indexOf(sessionName) + 1));
292                XMLUtils.startElement(handler, "session", sessionAttrs);
293                
294                
295                for (Integer entryPosition : allEntryPositions)
296                {
297                    if (!entryPositions1.contains(entryPosition))
298                    {
299                        /* Entrée ajoutée */
300                        _saxSessionEntry(handler, changes.getContent2(), sessionName, entryPosition, ChangeType.ADDED);
301                    }
302                    else if (!entryPositions2.contains(entryPosition))
303                    {
304                        /* Entrée supprimée */
305                        _saxSessionEntry(handler, changes.getContent1(), sessionName, entryPosition, ChangeType.REMOVED);
306                    }
307                    else if (changes.areEquals() || !_hasChanges(changes, sessionName, entryPosition))
308                    {
309                        /* Pas de changement */
310                        _saxSessionEntry(handler, changes.getContent2(), sessionName, entryPosition, null);
311                    }
312                    else
313                    {
314                        /* Entrée modifiée */
315                        boolean sameNature = _hasSameNature(changes, sessionName, entryPosition);
316                        _saxSessionEntry(handler, changes.getContent1(), sessionName, entryPosition, sameNature ? ChangeType.MODIFIED_OLD : ChangeType.REMOVED);
317                        _saxSessionEntry(handler, changes.getContent2(), sessionName, entryPosition, sameNature ? ChangeType.MODIFIED_NEW : ChangeType.ADDED);
318                    }
319                }
320
321                XMLUtils.endElement(handler, "session");
322            }
323        }
324        
325        XMLUtils.endElement(handler, "mcc");
326    }
327    
328    private boolean _hasSameNature(ContentComparatorResult result, String sessionName, Integer entryPosition)
329    {
330        ContentValue nature1 = result.getContent1().getRepeater(sessionName).getEntry(entryPosition).getValue("natureEnseignement");
331        ContentValue nature2 = result.getContent2().getRepeater(sessionName).getEntry(entryPosition).getValue("natureEnseignement");
332        
333        return Objects.equals(nature1, nature2);
334    }
335    
336    private boolean _hasChanges(ContentComparatorResult result, String sessionName, Integer entryPosition)
337    {
338        String beginMetadataPath = sessionName + "[" + entryPosition + "]" + ModelItem.ITEM_PATH_SEPARATOR;
339        for (ContentComparatorChange change : result.getChanges())
340        {
341            if (change.getPath().startsWith(beginMetadataPath))
342            {
343                return true;
344            }
345        }
346        return false;
347    }
348    
349    private void _saxSessionEntry(TransformerHandler handler, Content content, String sessionName, Integer entryPosition, ChangeType changeType) throws SAXException
350    {
351        ModelAwareRepeaterEntry sessionEntry = content.getRepeater(sessionName).getEntry(entryPosition);
352        
353        AttributesImpl entryAttrs = new AttributesImpl();
354        entryAttrs.addCDATAAttribute("name", entryPosition.toString());
355        
356        // nature enseignement
357        String natureEns = ContentDataHelper.getContentIdFromContentData(sessionEntry, "natureEnseignement");
358        if (StringUtils.isNotEmpty(natureEns))
359        {
360            entryAttrs.addCDATAAttribute("natureEnseignement", natureEns);
361        }
362        if (changeType != null)
363        {
364            entryAttrs.addCDATAAttribute("changeType", changeType.name().toLowerCase());
365        }
366        
367        // start entry
368        XMLUtils.startElement(handler, "entry", entryAttrs);
369        
370        saxSessionEntryDetails(handler, sessionEntry);
371        
372        // end entry
373        XMLUtils.endElement(handler, "entry");
374    }
375    
376    @Override
377    protected void saxGlobalInformations(TransformerHandler handler, Program program) throws SAXException
378    {
379        XMLUtils.createElement(handler, "catalog", program.getCatalog());
380        XMLUtils.createElement(handler, "catalogOld", _oldCatalog);
381    }
382}