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}