001/* 002 * Copyright 2020 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.version; 017 018import java.util.List; 019import java.util.Objects; 020import java.util.function.Predicate; 021import java.util.stream.Stream; 022 023import org.apache.avalon.framework.component.Component; 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.avalon.framework.service.Serviceable; 027import org.apache.commons.lang3.ArrayUtils; 028import org.apache.commons.lang3.StringUtils; 029 030import org.ametys.cms.content.compare.ContentComparator; 031import org.ametys.cms.content.compare.ContentComparatorChange; 032import org.ametys.cms.content.compare.ContentComparatorResult; 033import org.ametys.cms.data.type.ModelItemTypeConstants; 034import org.ametys.cms.data.type.ResourceElementTypeHelper; 035import org.ametys.cms.repository.Content; 036import org.ametys.core.util.LambdaUtils; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.plugins.repository.AmetysRepositoryException; 039import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 040import org.ametys.runtime.model.ModelItem; 041 042import com.google.common.base.Predicates; 043 044/** 045 * Helper for retrieving some useful version informations about {@link VersionAwareAmetysObject}s, and compare some versions. 046 */ 047public class CompareVersionHelper implements Component, Serviceable 048{ 049 /** Avalon Role */ 050 public static final String ROLE = CompareVersionHelper.class.getName(); 051 052 /** The ametys object resolver */ 053 protected AmetysObjectResolver _ametysObjectResolver; 054 /** The content comparator */ 055 protected ContentComparator _contentComparator; 056 057 @Override 058 public void service(ServiceManager manager) throws ServiceException 059 { 060 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 061 _contentComparator = (ContentComparator) manager.lookup(ContentComparator.ROLE); 062 } 063 064 /** 065 * Gets the current version of the given {@link VersionAwareAmetysObject} 066 * @param versionable The {@link VersionAwareAmetysObject} 067 * @return The current version 068 */ 069 public String getNewestVersion(VersionAwareAmetysObject versionable) 070 { 071 String[] allRevisions = versionable.getAllRevisions(); 072 if (allRevisions.length < 1) 073 { 074 throw new IllegalStateException(String.format("An unexpected error occured, the versionable content '%s' has no revision.", versionable)); 075 } 076 String revision = allRevisions[allRevisions.length - 1]; 077 return revision; 078 } 079 080 /** 081 * Gets the current version of the given {@link VersionAwareAmetysObject} 082 * @param versionable The {@link VersionAwareAmetysObject} 083 * @return The current version 084 */ 085 public String getCurrentVersion(VersionAwareAmetysObject versionable) 086 { 087 String currentRevision = versionable.getRevision(); 088 if (currentRevision != null) 089 { 090 return currentRevision; 091 } 092 093 return getNewestVersion(versionable); 094 } 095 096 /** 097 * Gets the previous version of the given {@link VersionAwareAmetysObject} 098 * @param versionable The {@link VersionAwareAmetysObject} 099 * @return The previous version. Can be null. 100 */ 101 public String getPreviousVersion(VersionAwareAmetysObject versionable) 102 { 103 String[] allRevisions = versionable.getAllRevisions(); 104 105 String currentRevision = versionable.getRevision(); 106 if (currentRevision != null) 107 { 108 for (int i = 0; i < allRevisions.length - 1; i++) 109 { 110 if (StringUtils.equals(allRevisions[i], currentRevision)) 111 { 112 if (i > 0) 113 { 114 return allRevisions[i - 1]; 115 } 116 else 117 { 118 return null; 119 } 120 } 121 } 122 return null; 123 } 124 else if (allRevisions.length > 2) 125 { 126 return allRevisions[allRevisions.length - 2]; 127 } 128 else 129 { 130 return null; 131 } 132 } 133 134 /** 135 * Gets the last version of the given {@link VersionAwareAmetysObject} with the given label 136 * @param versionable The {@link VersionAwareAmetysObject} 137 * @param label The label 138 * @return the last version 139 */ 140 public String getLastVersionWithLabel(VersionAwareAmetysObject versionable, String label) 141 { 142 String[] allRevisions = versionable.getAllRevisions(); 143 String stopAtRevision = versionable.getRevision(); 144 for (int i = allRevisions.length - 1; i >= 0; i--) 145 { 146 String revision = allRevisions[i]; 147 String[] revisionLabels = versionable.getLabels(revision); 148 if (ArrayUtils.contains(revisionLabels, label)) 149 { 150 return revision; 151 } 152 if (StringUtils.equals(revision, stopAtRevision)) 153 { 154 return null; 155 } 156 } 157 158 return null; 159 } 160 161 /** 162 * Compare two versions of the same content 163 * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content} 164 * @param versionableContentId The {@link Content} id to compare, which is {@link VersionAwareAmetysObject} 165 * @param baseVersion The base version 166 * @param version The version to be compared 167 * @return The {@link ContentComparatorResult} 168 * @throws AmetysRepositoryException repository exception 169 */ 170 public <C extends Content & VersionAwareAmetysObject> ContentComparatorResult compareVersions(String versionableContentId, String baseVersion, String version) throws AmetysRepositoryException 171 { 172 C baseContent = getContentVersion(versionableContentId, baseVersion); 173 C targetContent = getContentVersion(versionableContentId, version); 174 ContentComparatorResult comparatorResult = _contentComparator.compare(targetContent/* the 1st content is the target */, baseContent/* the 2nd is the base */); 175 return comparatorResult; 176 } 177 178 /** 179 * Gets the given version of the given {@link Content} od 180 * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content} 181 * @param contentId The {@link Content} id 182 * @param version The version. Can be null for current version 183 * @return the given version of the given {@link Content} od 184 */ 185 public <C extends Content & VersionAwareAmetysObject> C getContentVersion(String contentId, String version) 186 { 187 C versionableContent = _ametysObjectResolver.resolveById(contentId); 188 versionableContent.switchToRevision(version); 189 return versionableContent; 190 } 191 192 /** 193 * Gets the attribute changes (as {@link ModelItem}s) from the given {@link ContentComparatorChange}s (as a {@link ContentComparatorChange} path can be more detailed than a {@link ModelItem#getPath model item path}) 194 * <br>The false positive changes are also {@link #filterChanges filtered} 195 * @param changes The {@link ContentComparatorChange}s 196 * @return The changed {@link ModelItem}s 197 */ 198 public Stream<ModelItem> getChangedModelItems(List<ContentComparatorChange> changes) 199 { 200 return _filterChanges(changes) 201 .distinct() // some changes are different but are about the same ModelItem => remove those duplicates 202 .map(attributeChange -> attributeChange._contentComparatorChange) 203 .map(ContentComparatorChange::getModelItem); 204 } 205 206 /** 207 * Gets the {@link ContentComparatorChange}s, with some of them filtered (such as {@link ResourceElementTypeHelper#LAST_MODIFICATION_DATE_REPO_DATA_NAME someRichText/lastModified}), 208 * because they can be considered as "false positive", from the given {@link ContentComparatorChange}s 209 * @param changes The {@link ContentComparatorChange}s 210 * @return The filtered changed {@link ContentComparatorChange}s 211 */ 212 public Stream<ContentComparatorChange> filterChanges(List<ContentComparatorChange> changes) 213 { 214 return _filterChanges(changes) 215 .map(attributeChange -> attributeChange._contentComparatorChange); 216 } 217 218 private Stream<AttributeModelChange> _filterChanges(List<ContentComparatorChange> changes) 219 { 220 return changes 221 .stream() 222 .map(LambdaUtils.wrap(contentComparatorChange -> _getAttributeModelChange(contentComparatorChange))) 223 .filter(_attributeModelChangeFilters()); // filter out some changes, such as "someRichTextAttribute/lastModified" 224 } 225 226 private AttributeModelChange _getAttributeModelChange(ContentComparatorChange contentComparatorChange) 227 { 228 return new AttributeModelChange(contentComparatorChange); 229 } 230 231 private static Predicate<AttributeModelChange> _attributeModelChangeFilters() 232 { 233 return Predicates.compose(CompareVersionHelper::_filterContentComparatorChange, attributeModelChange -> attributeModelChange._contentComparatorChange); 234 } 235 236 private static boolean _filterContentComparatorChange(ContentComparatorChange contentComparatorChange) 237 { 238 return !_isLastModifiedOnRichText(contentComparatorChange); // hardcoded filter on "someRichTextAttribute/lastModified" 239 // here we can add some other static filters... 240 } 241 242 private static boolean _isLastModifiedOnRichText(ContentComparatorChange contentComparatorChange) 243 { 244 ModelItem modelItem = contentComparatorChange.getModelItem(); 245 return ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()) 246 && contentComparatorChange.getDetailDataPath().equals(ResourceElementTypeHelper.LAST_MODIFICATION_DATE_REPO_DATA_NAME); 247 } 248 249 private static class AttributeModelChange 250 { 251 /** The change (is more detailed than on a specific attribute) given by {@link ContentComparator} */ 252 private final ContentComparatorChange _contentComparatorChange; 253 /** The path of the involved model item */ 254 private final String _modelItemPath; 255 256 AttributeModelChange(ContentComparatorChange change) 257 { 258 _contentComparatorChange = change; 259 _modelItemPath = _contentComparatorChange.getModelItem().getPath(); 260 } 261 262 @Override 263 public int hashCode() 264 { 265 return Objects.hash(_modelItemPath); 266 } 267 268 // consider equals if it concerns the same modelItem (i.e. for a change about a/b/c and a/b/d, if the modelItem is 'a/b', it is the same as we are just interested in attribute change) 269 @Override 270 public boolean equals(Object obj) 271 { 272 if (this == obj) 273 { 274 return true; 275 } 276 if (obj == null) 277 { 278 return false; 279 } 280 if (getClass() != obj.getClass()) 281 { 282 return false; 283 } 284 AttributeModelChange other = (AttributeModelChange) obj; 285 return Objects.equals(_modelItemPath, other._modelItemPath); 286 } 287 } 288}