001/* 002 * Copyright 2017 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.cms.content.compare; 017 018import java.util.Arrays; 019import java.util.List; 020import java.util.Optional; 021 022import org.apache.avalon.framework.component.Component; 023import org.apache.commons.lang3.StringUtils; 024 025import org.ametys.cms.repository.Content; 026import org.ametys.plugins.repository.AmetysRepositoryException; 027import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 028import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 029import org.ametys.plugins.repository.data.holder.group.Repeater; 030import org.ametys.plugins.repository.data.holder.group.RepeaterEntry; 031import org.ametys.plugins.repository.data.type.ModelItemTypeConstants; 032import org.ametys.runtime.model.ElementDefinition; 033import org.ametys.runtime.model.ModelItem; 034import org.ametys.runtime.model.ModelViewItem; 035import org.ametys.runtime.model.ModelViewItemGroup; 036import org.ametys.runtime.model.SimpleViewItemGroup; 037import org.ametys.runtime.model.ViewHelper; 038import org.ametys.runtime.model.ViewItem; 039import org.ametys.runtime.model.ViewItemAccessor; 040import org.ametys.runtime.model.compare.DataChangeType; 041import org.ametys.runtime.plugin.component.AbstractLogEnabled; 042 043/** 044 * Object used to compare two contents 045 * 046 */ 047public class ContentComparator extends AbstractLogEnabled implements Component 048{ 049 /** The Avalon role */ 050 public static final String ROLE = ContentComparator.class.getName(); 051 052 /** 053 * Compare 2 data holders, for all their common attributes (contents with different contentTypes are rejected) 054 * @param dataHolder1 1st data holder 055 * @param dataHolder2 2nd data holder 056 * @return {@link ContentComparatorResult} 057 * @throws AmetysRepositoryException repository exception 058 */ 059 public ContentComparatorResult compare(ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2) throws AmetysRepositoryException 060 { 061 return compare(dataHolder1, dataHolder2, ComparisonContext.newInstance()); 062 } 063 064 /** 065 * Compare 2 data holders, for all their common attributes 066 * @param dataHolder1 1st data holder 067 * @param dataHolder2 2nd data holder 068 * @param comparatorContext context of the comparison 069 * @return {@link ContentComparatorResult} 070 * @throws AmetysRepositoryException repository exception 071 */ 072 public ContentComparatorResult compare(ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2, ComparisonContext comparatorContext) throws AmetysRepositoryException 073 { 074 return _compareContentsPart(dataHolder1, dataHolder2, comparatorContext); 075 } 076 077 private ContentComparatorResult _compareContentsPart(ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2, ComparisonContext comparatorContext) throws AmetysRepositoryException 078 { 079 Content content1 = (Content) dataHolder1.getRootDataHolder(); 080 Content content2 = (Content) dataHolder2.getRootDataHolder(); 081 082 // If the view item accessor is null, it returns the full model 083 ViewItemAccessor usedViewItemAccessor = Optional.ofNullable(comparatorContext.getViewItemAccessor()) 084 .orElseGet(() -> ViewHelper.createViewItemAccessor(dataHolder1.getModel())); 085 086 ContentComparatorResult result = new ContentComparatorResult(dataHolder1, dataHolder2, usedViewItemAccessor); 087 088 // If strictCompare, we check if the types are strictly equals 089 if (comparatorContext.strictCompare() && _typesAreDifferent(content1, content2)) 090 { 091 result.setNotEquals(); 092 return result; 093 } 094 else 095 { 096 // Iterate on items of the view accessor 097 for (ViewItem viewItem : usedViewItemAccessor.getViewItems()) 098 { 099 _compareDataHolders(result, dataHolder1, dataHolder2, viewItem, StringUtils.EMPTY); 100 } 101 } 102 103 return result; 104 } 105 106 private boolean _typesAreDifferent(Content content1, Content content2) 107 { 108 List<String> types1 = Arrays.asList(content1.getTypes()); 109 List<String> types2 = Arrays.asList(content2.getTypes()); 110 return types1.size() != types2.size() || !types1.containsAll(types2) || !types2.containsAll(types1); 111 } 112 113 /** 114 * Compare 2 model items and add the differences in result 115 * @param result global result where differences will be stored 116 * @param dataHolder1 1st data holder 117 * @param dataHolder2 2nd data holder 118 * @param viewItem view item that will be compared 119 * @param parentDataPath absolute data path of the container of the current view item 120 * @throws AmetysRepositoryException repository exception 121 */ 122 private void _compareDataHolders(ContentComparatorResult result, ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2, ViewItem viewItem, String parentDataPath) throws AmetysRepositoryException 123 { 124 // If the view item is a reference to a model item, compare the model items and their values 125 if (viewItem instanceof ModelViewItem modelViewItem) 126 { 127 ModelItem modelItemFromView = modelViewItem.getDefinition(); 128 String modelItemName = modelItemFromView.getName(); 129 130 ModelItem modelItem1 = null; 131 if (dataHolder1.hasDefinition(modelItemName)) 132 { 133 modelItem1 = dataHolder1.getDefinition(modelItemName); 134 } 135 136 ModelItem modelItem2 = null; 137 if (dataHolder2.hasDefinition(modelItemName)) 138 { 139 modelItem2 = dataHolder2.getDefinition(modelItemName); 140 } 141 142 if (modelItem1 != null && modelItem2 != null && modelItem1.getModel().equals(modelItem2.getModel())) 143 { 144 String currentDataPath = StringUtils.isNotEmpty(parentDataPath) ? parentDataPath + ModelItem.ITEM_PATH_SEPARATOR + modelItemName : modelItemName; 145 if (_checkModelItemPresence(result, dataHolder1, dataHolder2, modelItemFromView, currentDataPath, true)) 146 { 147 if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(modelItemFromView.getType().getId())) 148 { 149 _compareComposite(result, (ModelViewItemGroup) modelViewItem, dataHolder1.getComposite(modelItemName), dataHolder2.getComposite(modelItemName), modelItemFromView, currentDataPath); 150 } 151 else if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(modelItemFromView.getType().getId())) 152 { 153 _compareRepeater(result, (ModelViewItemGroup) modelViewItem, dataHolder1.getRepeater(modelItemName), dataHolder2.getRepeater(modelItemName), modelItemFromView, currentDataPath); 154 } 155 else 156 { 157 // If the model item is not a group, it has to be an ElementDefinition 158 _compareElement(result, dataHolder1, dataHolder2, (ElementDefinition) modelItemFromView, currentDataPath); 159 } 160 } 161 } 162 else 163 { 164 // TODO CMS-9978 Ignore or add the difference? What happens if an item is defined by a content but not by the other one? 165 // TODO CMS-9978 Shouldn't we compare the definitions? (cardinality, ...)? 166 } 167 } 168 // If the view item contains other view items, compare the children 169 else if (viewItem instanceof SimpleViewItemGroup simpleViewItemGroup) 170 { 171 for (ViewItem child : simpleViewItemGroup.getViewItems()) 172 { 173 _compareDataHolders(result, dataHolder1, dataHolder2, child, parentDataPath); 174 } 175 } 176 } 177 178 /** 179 * Check if a model item is present in both contents and add a change if requested 180 * @param result The result 181 * @param dataHolder1 1st data holder 182 * @param dataHolder2 2nd data holder 183 * @param dataPath absolute path of the data to check (also used to log) 184 * @param logChange true to add a change in the list (if one has it and the other doesn't) 185 * @return true if both contents have the node 186 */ 187 private boolean _checkModelItemPresence(ContentComparatorResult result, ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2, ModelItem definition, String dataPath, boolean logChange) 188 { 189 String modelItemName = definition.getName(); 190 191 boolean hasDataHolder1Value = dataHolder1.hasValue(modelItemName); 192 boolean hasDataHolder2Value = dataHolder2.hasValue(modelItemName); 193 194 if (hasDataHolder1Value && hasDataHolder2Value) 195 { 196 return true; 197 } 198 else if (logChange && hasDataHolder1Value && !hasDataHolder2Value) 199 { 200 result.addChange(new ContentComparatorChange(dataPath, StringUtils.EMPTY, definition, DataChangeType.REMOVED)); 201 } 202 else if (logChange && !hasDataHolder1Value && hasDataHolder2Value) 203 { 204 result.addChange(new ContentComparatorChange(dataPath, StringUtils.EMPTY, definition, DataChangeType.ADDED)); 205 } 206 207 return false; 208 } 209 210 /** 211 * Compare 2 composites 212 * @param result global result where differences will be stored 213 * @param viewItemGroup view item of the current composite 214 * @param composite1 1st composite 215 * @param composite2 2nd composite 216 * @param definition definition of the composite 217 * @param compositeDataPath absolute data path of the current composite 218 * @throws AmetysRepositoryException repository exception 219 */ 220 private void _compareComposite(ContentComparatorResult result, ModelViewItemGroup viewItemGroup, ModelAwareComposite composite1, ModelAwareComposite composite2, ModelItem definition, String compositeDataPath) throws AmetysRepositoryException 221 { 222 for (ViewItem viewItem : viewItemGroup.getViewItems()) 223 { 224 _compareDataHolders(result, composite1, composite2, viewItem, compositeDataPath); 225 } 226 } 227 228 /** 229 * Compare 2 repeaters 230 * @param result global result where differences will be stored 231 * @param viewItemGroup view item of the current repeater 232 * @param repeater1 1st repeater 233 * @param repeater2 2nd repeater 234 * @param definition definition of the repeater 235 * @param repeaterDataPath absolute data path of the current repeater 236 * @throws AmetysRepositoryException repository exception 237 */ 238 private void _compareRepeater(ContentComparatorResult result, ModelViewItemGroup viewItemGroup, Repeater repeater1, Repeater repeater2, ModelItem definition, String repeaterDataPath) throws AmetysRepositoryException 239 { 240 if (repeater1.getSize() > repeater2.getSize()) 241 { 242 for (int i = repeater2.getSize() + 1; i <= repeater1.getSize(); i++) 243 { 244 result.addChange(new ContentComparatorChange(repeaterDataPath + "[" + Integer.toString(i) + "]", StringUtils.EMPTY, definition, DataChangeType.REMOVED)); 245 } 246 } 247 else if (repeater1.getSize() < repeater2.getSize()) 248 { 249 for (int i = repeater1.getSize() + 1; i <= repeater2.getSize(); i++) 250 { 251 result.addChange(new ContentComparatorChange(repeaterDataPath + "[" + Integer.toString(i) + "]", StringUtils.EMPTY, definition, DataChangeType.ADDED)); 252 } 253 } 254 255 // Compare common entries 256 int numberOfCommonEntries = Math.min(repeater1.getSize(), repeater2.getSize()); 257 for (int i = 1; i <= numberOfCommonEntries; i++) 258 { 259 String repeaterEntryDataPath = repeaterDataPath + "[" + Integer.toString(i) + "]"; 260 261 RepeaterEntry repeaterEntry1 = repeater1.getEntry(i); 262 RepeaterEntry repeaterEntry2 = repeater2.getEntry(i); 263 264 // Compare repeater's children for each entry 265 for (ViewItem viewItem : viewItemGroup.getViewItems()) 266 { 267 _compareDataHolders(result, repeaterEntry1, repeaterEntry2, viewItem, repeaterEntryDataPath); 268 } 269 } 270 } 271 272 /** 273 * Compare 2 elements 274 * @param result global result where differences will be stored 275 * @param dataHolder1 1st data holder 276 * @param dataHolder2 2nd data holder 277 * @param definition definition of the attribute 278 * @param elementPath absolute data path of the current attribute 279 * @throws AmetysRepositoryException repository exception 280 */ 281 private void _compareElement(ContentComparatorResult result, ModelAwareDataHolder dataHolder1, ModelAwareDataHolder dataHolder2, ElementDefinition<?> definition, String elementPath) throws AmetysRepositoryException 282 { 283 String elementName = definition.getName(); 284 285 definition.getType() 286 .compareValues(dataHolder1.getValue(elementName), dataHolder2.getValue(elementName)) 287 .map(change -> 288 { 289 String detailPath = change.getRight(); 290 return new ContentComparatorChange(elementPath, detailPath, definition, change.getLeft(), change.getMiddle()); 291 }) 292 .forEach(result::addChange); 293 } 294 295 /** 296 * Comparison context 297 */ 298 public static final class ComparisonContext 299 { 300 private boolean _strictCompare = true; 301 private ViewItemAccessor _viewItemAccessor; 302 303 private ComparisonContext() 304 { 305 // Nothing to do 306 } 307 308 /** 309 * Create an instance of comparison context 310 * @return the comparison context 311 */ 312 public static ComparisonContext newInstance() 313 { 314 return new ComparisonContext(); 315 } 316 317 /** 318 * Set to <code>false</code> to accept contents with different content types 319 * @param strictCompare <code>true</code> to reject contents with different content types 320 * @return itself 321 */ 322 public ComparisonContext withStrictCompare(boolean strictCompare) 323 { 324 _strictCompare = strictCompare; 325 return this; 326 } 327 328 /** 329 * Set the {@link ViewItemAccessor} used for the comparison 330 * @param viewItemAccessor the view item accessor 331 * @return itself 332 */ 333 public ComparisonContext withViewItemAccessor(ViewItemAccessor viewItemAccessor) 334 { 335 _viewItemAccessor = viewItemAccessor; 336 return this; 337 } 338 339 /** 340 * Get the strict compare status, <code>true</code> by default. 341 * @return <code>true</code> to make a strict comparison 342 */ 343 public boolean strictCompare() 344 { 345 return _strictCompare; 346 } 347 348 /** 349 * Get the view item accessor to compare, can be null 350 * @return the view item accessor 351 */ 352 public ViewItemAccessor getViewItemAccessor() 353 { 354 return _viewItemAccessor; 355 } 356 } 357}