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