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