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}