001/*
002 *  Copyright 2022 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.plugins.repository.maintenance;
017
018import java.util.List;
019
020import javax.jcr.Node;
021import javax.jcr.RepositoryException;
022import javax.jcr.Session;
023import javax.jcr.SimpleCredentials;
024
025import org.apache.jackrabbit.core.RepositoryContext;
026import org.apache.jackrabbit.core.id.NodeId;
027import org.apache.jackrabbit.core.id.PropertyId;
028import org.apache.jackrabbit.core.persistence.IterablePersistenceManager;
029import org.apache.jackrabbit.core.persistence.pool.BundleDbPersistenceManager;
030import org.apache.jackrabbit.core.state.ItemStateException;
031import org.apache.jackrabbit.core.state.NodeReferences;
032import org.slf4j.LoggerFactory;
033
034import org.ametys.workspaces.repository.maintenance.AbstractMaintenanceTask;
035
036/**
037 * Repository maintenance task that crawls every node and check that the parent references still exists 
038 */
039public class CleanReferenceTask extends AbstractMaintenanceTask
040{
041    private static final int __BUNDLE_SIZE = 100_000;
042    long _handled;
043    int _inconsistent;
044    int _partiallyInconsistent;
045    int _cleaned;
046    private Session _session;
047    private NodeId _last;
048
049    @Override
050    public boolean requiresOffline()
051    {
052        return false;
053    }
054    
055    @Override
056    protected void initialize() throws RepositoryException
057    {
058        _session = getOrCreateRepository().login(new SimpleCredentials("__MAINTENANCE_TASK__", "".toCharArray()));
059    }
060    
061    @Override
062    protected void apply() throws RepositoryException
063    {
064        RepositoryContext repositoryContext = getOrCreateRepositoryContext();
065        List<IterablePersistenceManager> pmList = getAllPersistenceManager(repositoryContext);
066        
067        for (IterablePersistenceManager pm : pmList)
068        {
069            if (pm instanceof BundleDbPersistenceManager bundleDbPersistenceManager)
070            {
071                _logger.info("Cleaning nodes for persistence manager {}", pm);
072                _cleanReferences(bundleDbPersistenceManager);
073            }
074            else
075            {
076                _logger.info("The persistence manager {} doesn't support the operation. It will be skipped.", pm.getClass().getName());
077            }
078        }
079    }    
080    
081    private void _cleanReferences(BundleDbPersistenceManager bundleDbPersistenceManager) throws RepositoryException
082    {
083        try
084        {
085            // initialized the report
086            _handled = 0;
087            _inconsistent = 0;
088            _partiallyInconsistent = 0;
089            _cleaned = 0;
090            
091            _last = null;
092            // if this fail then we can't do anything so we just throw the exception
093            List<NodeId> allNodeIds = bundleDbPersistenceManager.getAllNodeIds(_last, __BUNDLE_SIZE);
094            long total = 0L;
095            while (!allNodeIds.isEmpty())
096            {
097                total += allNodeIds.size();
098                
099                for (NodeId id : allNodeIds)
100                {
101                    _last = id;
102                    try
103                    {
104                        if (bundleDbPersistenceManager.existsReferencesTo(id))
105                        {
106                            _cleanReferences(id, bundleDbPersistenceManager);
107                        }
108                    }
109                    catch (ItemStateException e)
110                    {
111                        _logger.warn("Failed to retrieve references to node " + id.toString() + ". The node will be skipped.", e);
112                    }
113                }
114                
115                _logger.info(String.format("%,d nodes processed...", total));
116                
117                // if this fail then we can't do anything so we just throw the exception
118                allNodeIds = bundleDbPersistenceManager.getAllNodeIds(_last, __BUNDLE_SIZE);
119            }
120            
121            _logger.info(String.format("The operation is over. Out of %,d nodes found, %,d had references.%n"
122                    + "%,d inconsistent nodes and %,d partially inconsistent nodes were found.%n"
123                    + "%,d have been removed.", total, _handled, _inconsistent, _partiallyInconsistent, _cleaned));
124        }
125        catch (ItemStateException e)
126        {
127            _logger.error("Failed to retrieve the node ids. Can't clean the reference", e);
128            throw new RepositoryException("Failed to retrieve the node ids. Can't clean the reference", e);
129        }
130    }
131    
132    private void _cleanReferences(NodeId nodeId, BundleDbPersistenceManager persistenceManager)
133    {
134        try
135        {
136            NodeReferences nodeRefs = persistenceManager.loadReferencesTo(nodeId);
137            List<PropertyId> refs = nodeRefs.getReferences();
138            final int totalRefs = refs.size();
139            int localInconsistent = 0;
140            
141            for (PropertyId ref: refs)
142            {
143                String uuid = ref.getParentId().toString();
144                try
145                {
146                    _session.getNodeByIdentifier(uuid);
147                }
148                catch (RepositoryException e)
149                {
150                    _logger.debug("Inconsistent reference: " + nodeId.toString() + " <- " + uuid);
151                    localInconsistent++;
152                }
153            }
154            
155            if (localInconsistent > 0)
156            {
157                if (localInconsistent == totalRefs)
158                {
159                    _inconsistent++;
160                    try
161                    {
162                        Node node = _session.getNodeByIdentifier(nodeId.toString());
163                        _logger.debug("Inconsistent references to " + node.getPath() + " (" + nodeId.toString() + ") will be deleted");
164                        persistenceManager.destroy(nodeRefs);
165                        _cleaned++;
166                    }
167                    catch (RepositoryException e)
168                    {
169                        _logger.warn("Failed to retrieve node " + nodeId.toString() + ". "
170                                + "This node had inconsistencies. The node will be skipped and the process will continue.", e);
171                    }
172                    catch (ItemStateException e)
173                    {
174                        _logger.warn("Failed to destroy node references to inconsistent node " + nodeId.toString() + ". "
175                                + "The node will be skipped and the process will continue.", e);
176                    }
177                }
178                else
179                {
180                    _partiallyInconsistent++;
181                    _logger.info("Node " + nodeId.toString() + " is partially inconsistent. "
182                            + "Nothing has been implemented to handle those case. It will be ignored by the cleaning operation.");
183                }
184            }
185            _handled++;
186        }
187        catch (ItemStateException e)
188        {
189            _logger.warn("Failed to retrieve references to node " + nodeId.toString() + ". "
190                    + "The node will be skipped and the process will continue.", e);
191        }
192    }
193
194    @Override
195    protected void close()
196    {
197        _session.logout();
198        
199        super.close();
200    }
201        
202    @Override
203    protected void setLogger()
204    {
205        setLogger(LoggerFactory.getLogger(CleanReferenceTask.class));
206    }
207}