001/*
002 *  Copyright 2021 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.workspaces.repository.maintenance;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import javax.jcr.Credentials;
024import javax.jcr.NodeIterator;
025import javax.jcr.RepositoryException;
026import javax.jcr.Session;
027import javax.jcr.SimpleCredentials;
028import javax.jcr.query.Query;
029import javax.jcr.version.Version;
030import javax.jcr.version.VersionHistory;
031import javax.jcr.version.VersionIterator;
032
033import org.apache.jackrabbit.core.NodeImpl;
034import org.apache.jackrabbit.core.RepositoryImpl;
035import org.slf4j.LoggerFactory;
036
037/**
038 * Remove unused object from jcr history
039 */
040public class RemoveUnusedHistoryTask extends AbstractMaintenanceTask
041{
042    /** The JackRabbit RepositoryImpl */
043    protected RepositoryImpl _repository;
044
045    @Override
046    public boolean requiresOffline()
047    {
048        return false;
049    }
050    
051    @Override
052    protected void setLogger()
053    {
054        setLogger(LoggerFactory.getLogger(RemoveUnusedHistoryTask.class));
055    }
056
057    @Override
058    protected void apply() throws RepositoryException
059    {
060        Map<String, Session> sessions = null;
061        Credentials credentials = new SimpleCredentials("ametys", new char[]{});
062       
063        try
064        {
065            _repository = getOrCreateRepository();
066
067            String[] accessibleWorkspaceNames = _getWorkspaces(credentials);
068            sessions = _loadSessions(accessibleWorkspaceNames);
069            
070            @SuppressWarnings("deprecation")
071            Query q = sessions.get("default").getWorkspace().getQueryManager().createQuery("//element(*, nt:versionHistory)", Query.XPATH);
072            NodeIterator nodes = q.execute().getNodes();
073            long size = nodes.getSize();
074            
075            _progress = new TaskProgress(size * 2);
076            _progress.setRunning();
077
078            List<VersionHistory> vhs = _getVersionHistoryToRemove(sessions, accessibleWorkspaceNames, nodes);
079            _progress.progress(size - vhs.size());
080            
081            _logger.info("Going to remove " + vhs.size() + " history nodes on " + size + " existing nodes");
082            
083            _removeUnusedHistory(vhs, sessions);
084        }
085        catch (RepositoryException e)
086        {
087            if (_progress != null)
088            {
089                _progress.setInErrorState(e);
090            }
091            
092            throw e;
093        }
094        catch (Exception e)
095        {
096            if (_progress != null)
097            {
098                _progress.setInErrorState(e);
099            }
100            
101            throw new RuntimeException(e);
102        }
103        finally
104        {
105            _logout(sessions);
106            
107            if (_progress != null)
108            {
109                _progress.setFinished();
110            }
111            
112        }
113    }
114    
115    private void _logout(Map<String, Session> sessions)
116    {
117        if (sessions != null)
118        {
119            for (Session session : sessions.values())
120            {
121                session.logout();
122            }
123        }
124    }
125    
126    private String[] _getWorkspaces(Credentials credentials) throws RepositoryException
127    {
128        Session session = null;
129        try
130        {
131            session = _repository.login(credentials, "default");
132            return session.getWorkspace().getAccessibleWorkspaceNames();
133        }
134        finally 
135        {
136            if (session != null)
137            {
138                session.logout();
139            }
140        }
141    }
142    
143    private Map<String, Session> _loadSessions(String[] accessibleWorkspaceNames) throws RepositoryException
144    {
145        Map<String, Session> sessions = new HashMap<>();
146        Credentials credentials = new SimpleCredentials("ametys", new char[]{});
147       
148        for (String workspaceName: accessibleWorkspaceNames)
149        {
150            sessions.put(workspaceName, _repository.login(credentials, workspaceName));
151        }
152        
153        return sessions;
154    }
155    
156    private void _removeUnusedHistory(List<VersionHistory> versionHistories, Map<String, Session> sessions) throws RepositoryException
157    {
158        int errors = 0;
159        int done = 0;
160        int empty = 0;
161        int fixed = 0;
162
163        for (VersionHistory versionHistory : versionHistories)
164        {
165            VersionIterator it = versionHistory.getAllVersions();
166
167            boolean hasError = false;
168            int childNodesSize = 0;
169            
170            while (it.hasNext())
171            {
172                Version version = it.nextVersion();
173                if (!"jcr:rootVersion".equals(version.getName()))
174                {
175                    childNodesSize++;
176
177                    try
178                    {
179                        versionHistory.removeVersion(version.getName());
180                    }
181                    catch (RepositoryException e)
182                    {
183                        hasError = true;
184                        _logger.error("Error with version " + version.getName() + " of " + versionHistory.getIdentifier() + "... " + e);
185                    }
186                }
187            }
188            
189            if (childNodesSize == 0)
190            {
191                _removeEmptyHistory(versionHistory, sessions.get("default"));
192                try
193                {
194                    sessions.get("default").getNodeByIdentifier(versionHistory.getIdentifier());
195                    
196                    _logger.debug("Empty history node is " + versionHistory.getIdentifier());
197                    empty++;            
198                }
199                catch (RepositoryException e)
200                {
201                    fixed++;
202                }
203            }
204            else if (hasError)
205            {
206                errors++;
207            }
208            else
209            {
210                done++;
211            }
212            
213            _progress.progress();
214        }
215
216        _logger.info(done + " normal history nodes removed\n"
217                    + fixed + " empty history nodes fixed and removed\n"
218                    + empty + " empty history nodes that are unfixable\n"
219                    + errors + " unconsistents history nodes that cannot be removed");
220    }
221    
222    private void _removeEmptyHistory(VersionHistory vh, Session session) throws RepositoryException
223    {
224        var node = ((NodeImpl) session.getNode("/ametys:root")).addNodeWithUuid("historyrepair", "nt:unstructured", vh.getProperty("jcr:versionableUuid").getString());
225        node.addMixin("mix:versionable");
226        session.save();
227        session.getWorkspace().getVersionManager().checkin(node.getPath());
228        session.getWorkspace().getVersionManager().checkout(node.getPath());
229        session.removeItem("/ametys:root/historyrepair");
230        session.save();
231        vh.removeVersion("1.0");
232    }
233    
234    private List<VersionHistory> _getVersionHistoryToRemove(Map<String, Session> sessions, String[] accessibleWorkspaceNames, NodeIterator nodes) throws RepositoryException
235    {
236        List<VersionHistory> vhs = new ArrayList<>();
237        
238        while (nodes.hasNext())
239        {
240            VersionHistory versionHistory = (VersionHistory) nodes.nextNode();
241            String versionableIdentifier = versionHistory.getVersionableIdentifier();
242            
243            var foundOne = false;
244            for (var i = 0; i < accessibleWorkspaceNames.length; i++)
245            {
246                var workspaceName = accessibleWorkspaceNames[i];
247            
248                try
249                {
250                    sessions.get(workspaceName).getNodeByIdentifier(versionableIdentifier);
251                    foundOne = true;
252                    break;
253                }
254                catch (RepositoryException e)
255                {
256                    // nothing
257                }
258            }
259        
260            if (!foundOne)
261            {
262                vhs.add(versionHistory);
263            }                
264            _progress.progress();
265        }
266        
267        return vhs;
268    }
269
270}