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