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.Optional; 022import java.util.function.Predicate; 023import java.util.stream.Stream; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang3.ArrayUtils; 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 getCurrentVersion(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 previous version of the given {@link VersionAwareAmetysObject} 083 * @param versionable The {@link VersionAwareAmetysObject} 084 * @return The previous version 085 */ 086 public Optional<String> getPreviousVersion(VersionAwareAmetysObject versionable) 087 { 088 return Optional.of(versionable.getAllRevisions()) 089 .filter(allRevisions -> allRevisions.length >= 2) 090 .map(allRevisions -> allRevisions[allRevisions.length - 2]); 091 } 092 093 /** 094 * Gets the last version of the given {@link VersionAwareAmetysObject} with the given label 095 * @param versionable The {@link VersionAwareAmetysObject} 096 * @param label The label 097 * @return the last version 098 */ 099 public Optional<String> getLastVersionWithLabel(VersionAwareAmetysObject versionable, String label) 100 { 101 String[] allRevisions = versionable.getAllRevisions(); 102 for (int i = allRevisions.length - 1; i >= 0; i--) 103 { 104 String revision = allRevisions[i]; 105 String[] revisionLabels = versionable.getLabels(revision); 106 if (ArrayUtils.contains(revisionLabels, label)) 107 { 108 return Optional.of(revision); 109 } 110 } 111 112 return Optional.empty(); 113 } 114 115 /** 116 * Compare two versions of the same content 117 * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content} 118 * @param versionableContentId The {@link Content} id to compare, which is {@link VersionAwareAmetysObject} 119 * @param baseVersion The base version 120 * @param version The version to be compared 121 * @return The {@link ContentComparatorResult} 122 * @throws AmetysRepositoryException repository exception 123 * @throws IOException IO exception 124 */ 125 public <C extends Content & VersionAwareAmetysObject> ContentComparatorResult compareVersions(String versionableContentId, String baseVersion, String version) throws AmetysRepositoryException, IOException 126 { 127 C baseContent = getContentVersion(versionableContentId, baseVersion); 128 C targetContent = getContentVersion(versionableContentId, version); 129 ContentComparatorResult comparatorResult = _contentComparator.compare(targetContent/* the 1st content is the target */, baseContent/* the 2nd is the base */); 130 return comparatorResult; 131 } 132 133 /** 134 * Gets the given version of the given {@link Content} od 135 * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content} 136 * @param contentId The {@link Content} id 137 * @param version The version. Can be null for current version 138 * @return the given version of the given {@link Content} od 139 */ 140 public <C extends Content & VersionAwareAmetysObject> C getContentVersion(String contentId, String version) 141 { 142 C versionableContent = _ametysObjectResolver.resolveById(contentId); 143 versionableContent.switchToRevision(version); 144 return versionableContent; 145 } 146 147 /** 148 * 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}) 149 * <br>The false positive changes are also {@link #filterChanges filtered} 150 * @param changes The {@link ContentComparatorChange}s 151 * @return The changed {@link ModelItem}s 152 */ 153 public Stream<ModelItem> getChangedModelItems(List<ContentComparatorChange> changes) 154 { 155 return _filterChanges(changes) 156 .distinct() // some changes are different but are about the same ModelItem => remove those duplicates 157 .map(attributeChange -> attributeChange._contentComparatorChange) 158 .map(ContentComparatorChange::getModelItem); 159 } 160 161 /** 162 * Gets the {@link ContentComparatorChange}s, with some of them filtered (such as {@link ResourceElementTypeHelper#LAST_MODIFICATION_DATE_IDENTIFIER someRichText/lastModified}), 163 * because they can be considered as "false positive", from the given {@link ContentComparatorChange}s 164 * @param changes The {@link ContentComparatorChange}s 165 * @return The filtered changed {@link ContentComparatorChange}s 166 */ 167 public Stream<ContentComparatorChange> filterChanges(List<ContentComparatorChange> changes) 168 { 169 return _filterChanges(changes) 170 .map(attributeChange -> attributeChange._contentComparatorChange); 171 } 172 173 private Stream<AttributeModelChange> _filterChanges(List<ContentComparatorChange> changes) 174 { 175 return changes 176 .stream() 177 .map(LambdaUtils.wrap(contentComparatorChange -> _getAttributeModelChange(contentComparatorChange))) 178 .filter(_attributeModelChangeFilters()); // filter out some changes, such as "someRichTextAttribute/lastModified" 179 } 180 181 private AttributeModelChange _getAttributeModelChange(ContentComparatorChange contentComparatorChange) 182 { 183 return new AttributeModelChange(contentComparatorChange); 184 } 185 186 private static Predicate<AttributeModelChange> _attributeModelChangeFilters() 187 { 188 return Predicates.compose(CompareVersionHelper::_filterContentComparatorChange, attributeModelChange -> attributeModelChange._contentComparatorChange); 189 } 190 191 private static boolean _filterContentComparatorChange(ContentComparatorChange contentComparatorChange) 192 { 193 return !_isLastModifiedOnRichText(contentComparatorChange); // hardcoded filter on "someRichTextAttribute/lastModified" 194 // here we can add some other static filters... 195 } 196 197 private static boolean _isLastModifiedOnRichText(ContentComparatorChange contentComparatorChange) 198 { 199 ModelItem modelItem = contentComparatorChange.getModelItem(); 200 return ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()) 201 && contentComparatorChange.getDetailDataPath().equals(ResourceElementTypeHelper.LAST_MODIFICATION_DATE_IDENTIFIER); 202 } 203 204 private static class AttributeModelChange 205 { 206 /** The change (is more detailed than on a specific attribute) given by {@link ContentComparator} */ 207 private final ContentComparatorChange _contentComparatorChange; 208 /** The path of the involved model item */ 209 private final String _modelItemPath; 210 211 AttributeModelChange(ContentComparatorChange change) 212 { 213 _contentComparatorChange = change; 214 _modelItemPath = _contentComparatorChange.getModelItem().getPath(); 215 } 216 217 @Override 218 public int hashCode() 219 { 220 return Objects.hash(_modelItemPath); 221 } 222 223 // 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) 224 @Override 225 public boolean equals(Object obj) 226 { 227 if (this == obj) 228 { 229 return true; 230 } 231 if (obj == null) 232 { 233 return false; 234 } 235 if (getClass() != obj.getClass()) 236 { 237 return false; 238 } 239 AttributeModelChange other = (AttributeModelChange) obj; 240 return Objects.equals(_modelItemPath, other._modelItemPath); 241 } 242 } 243}