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