001/* 002 * Copyright 2011 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.workflow.purge; 017 018import java.time.Instant; 019import java.time.ZoneId; 020import java.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.ListIterator; 025import java.util.Set; 026 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.nodetype.NodeType; 030import javax.jcr.version.Version; 031import javax.jcr.version.VersionHistory; 032import javax.jcr.version.VersionIterator; 033import javax.jcr.version.VersionManager; 034 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.logger.AbstractLogEnabled; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.avalon.framework.service.Serviceable; 040 041import org.ametys.cms.repository.WorkflowAwareContent; 042import org.ametys.cms.repository.WorkflowAwareContentHelper; 043import org.ametys.plugins.repository.AmetysRepositoryException; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.runtime.config.Config; 046 047/** 048 * Component which purges content old versions. 049 */ 050public class PurgeVersionsManager extends AbstractLogEnabled implements Component, Serviceable 051{ 052 053 /** The avalon component role. */ 054 public static final String ROLE = PurgeVersionsManager.class.getName(); 055 056 /** The current step ID property. */ 057 private static final String __CURRENT_STEP_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + WorkflowAwareContentHelper.METADATA_CURRENT_STEP_ID; 058 059 @Override 060 public void service(ServiceManager manager) throws ServiceException 061 { 062 // Ignore. 063 } 064 065 /** 066 * Purge a content. 067 * @param content the content to purge. 068 * @param validationStepId the ID of the validation step for this content. 069 * @param firstVersionsToKeep the count of first versions to keep. 070 * @return the count of removed versions. 071 */ 072 public int purgeContent(WorkflowAwareContent content, long validationStepId, int firstVersionsToKeep) 073 { 074 try 075 { 076 Node node = content.getNode(); 077 078 if (node.isNodeType(NodeType.MIX_VERSIONABLE)) 079 { 080 // Retrieve the version history and base version of the content. 081 VersionManager versionManager = node.getSession().getWorkspace().getVersionManager(); 082 VersionHistory versionHistory = versionManager.getVersionHistory(node.getPath()); 083 Version baseVersion = versionManager.getBaseVersion(node.getPath()); 084 085 return purgeContent(content, versionHistory, baseVersion, validationStepId, firstVersionsToKeep); 086 } 087 088 return 0; 089 } 090 catch (RepositoryException e) 091 { 092 throw new AmetysRepositoryException("Error purging the content " + content.getId(), e); 093 } 094 } 095 096 /** 097 * Purge a content version history. 098 * @param content the content. 099 * @param versionHistory the version history to purge. 100 * @param baseVersion the content base version, must be kept. 101 * @param validationStepId the ID of the validation step for this content. 102 * @param firstVersionsToKeep the count of first versions to keep. 103 * @return the count of removed versions. 104 * @throws RepositoryException if an error occurs. 105 */ 106 protected int purgeContent(WorkflowAwareContent content, VersionHistory versionHistory, Version baseVersion, long validationStepId, int firstVersionsToKeep) throws RepositoryException 107 { 108 int validatedVersionsToKeep = Config.getInstance().getValueAsLong("purge.keep.validated.versions").intValue(); 109 int daysToKeep = Config.getInstance().getValueAsLong("purge.before.days").intValue(); 110 111 Set<String> versionsToRemove = new LinkedHashSet<>(); 112 113 ZonedDateTime limit = ZonedDateTime.now().minusDays(daysToKeep); 114 115 // Get all the versions. 116 List<VersionInfo> allVersions = getAllVersions(versionHistory); 117 118 ListIterator<VersionInfo> versionIt = allVersions.listIterator(allVersions.size()); 119 120 int validatedVersions = 0; 121 try 122 { 123 if (content.getCurrentStepId() == validationStepId) 124 { 125 validatedVersions++; 126 } 127 } 128 catch (AmetysRepositoryException e) 129 { 130 validatedVersions = 0; 131 } 132 133 while (versionIt.hasPrevious()) 134 { 135 VersionInfo info = versionIt.previous(); 136 137 if (validatedVersions >= validatedVersionsToKeep && info.getCreated().isBefore(limit) && versionIt.previousIndex() >= firstVersionsToKeep - 1) 138 { 139 // Do not remove the base version. 140 if (!info.getName().equals(baseVersion.getName())) 141 { 142 versionsToRemove.add(info.getName()); 143 } 144 } 145 146 if (info.getStepId() != null && info.getStepId() == validationStepId) 147 { 148 validatedVersions++; 149 } 150 } 151 152 for (String version : versionsToRemove) 153 { 154 versionHistory.removeVersion(version); 155 } 156 157 return versionsToRemove.size(); 158 } 159 160 /** 161 * Get all versions of a version history. 162 * @param versionHistory the version history to purge. 163 * @return all the version infos. 164 * @throws RepositoryException if an error occurs. 165 */ 166 protected List<VersionInfo> getAllVersions(VersionHistory versionHistory) throws RepositoryException 167 { 168 List<VersionInfo> allVersions = new ArrayList<>(); 169 170 VersionIterator versions = versionHistory.getAllVersions(); 171 172 while (versions.hasNext()) 173 { 174 Version version = versions.nextVersion(); 175 176 Node node = version.getFrozenNode(); 177 178 VersionInfo info = new VersionInfo(); 179 180 info.setName(version.getName()); 181 info.setCreated(version.getCreated().toInstant()); 182 183 if (node.hasProperty(__CURRENT_STEP_ID_PROPERTY)) 184 { 185 long stepId = node.getProperty(__CURRENT_STEP_ID_PROPERTY).getLong(); 186 info.setStepId(stepId); 187 } 188 189 allVersions.add(info); 190 } 191 192 return allVersions; 193 } 194 195 /** 196 * Version information. 197 */ 198 protected class VersionInfo 199 { 200 201 /** The version name. */ 202 protected String _name; 203 204 /** The version creation date. */ 205 protected ZonedDateTime _created; 206 207 /** The version step ID. */ 208 protected Long _stepId; 209 210 /** 211 * Get the name. 212 * @return the name 213 */ 214 public String getName() 215 { 216 return _name; 217 } 218 219 /** 220 * Set the name. 221 * @param name the name to set 222 */ 223 public void setName(String name) 224 { 225 this._name = name; 226 } 227 228 /** 229 * Get the created. 230 * @return the created 231 */ 232 public ZonedDateTime getCreated() 233 { 234 return _created; 235 } 236 237 /** 238 * Set the created. 239 * @param created the created to set 240 */ 241 public void setCreated(Instant created) 242 { 243 this._created = created.atZone(ZoneId.systemDefault()); 244 } 245 246 /** 247 * Get the stepId. 248 * @return the stepId 249 */ 250 public Long getStepId() 251 { 252 return _stepId; 253 } 254 255 /** 256 * Set the stepId. 257 * @param stepId the stepId to set 258 */ 259 public void setStepId(Long stepId) 260 { 261 this._stepId = stepId; 262 } 263 264 @Override 265 public String toString() 266 { 267 return _name; 268 } 269 270 } 271 272}