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;
024import javax.jcr.UnsupportedRepositoryOperationException;
025
026import org.apache.jackrabbit.core.id.NodeId;
027import org.apache.jackrabbit.core.id.PropertyId;
028import org.apache.jackrabbit.core.persistence.PersistenceManager;
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        PersistenceManager versionPersistenceManager = getOrCreateRepositoryContext().getInternalVersionManager().getPersistenceManager();
065        if (versionPersistenceManager instanceof BundleDbPersistenceManager bundleVersionPersistenceManager)
066        {
067            try
068            {
069                // initialized the report
070                _handled = 0;
071                _inconsistent = 0;
072                _partiallyInconsistent = 0;
073                _cleaned = 0;
074                
075                _last = null;
076                // if this fail then we can't do anything so we just throw the exception
077                List<NodeId> allNodeIds = bundleVersionPersistenceManager.getAllNodeIds(_last, __BUNDLE_SIZE);
078                long total = 0L;
079                while (!allNodeIds.isEmpty())
080                {
081                    total += allNodeIds.size();
082                    
083                    for (NodeId id : allNodeIds)
084                    {
085                        _last = id;
086                        try
087                        {
088                            if (bundleVersionPersistenceManager.existsReferencesTo(id))
089                            {
090                                _cleanReferences(id, bundleVersionPersistenceManager);
091                            }
092                        }
093                        catch (ItemStateException e)
094                        {
095                            _logger.warn("Failed to retrieve references to node " + id.toString() + ". The node will be skipped.", e);
096                        }
097                    }
098                    _logger.info(String.format("%,d nodes processed...", total));
099                    // if this fail then we can't do anything so we just throw the exception
100                    allNodeIds = bundleVersionPersistenceManager.getAllNodeIds(_last, __BUNDLE_SIZE);
101                }
102                
103                _logger.info(String.format("The operation is over. Out of %,d nodes found, %,d had references.%n"
104                        + "%,d inconsistent nodes and %,d partially inconsistent nodes were found.%n"
105                        + "%,d have been removed.", total, _handled, _inconsistent, _partiallyInconsistent, _cleaned));
106            }
107            catch (ItemStateException e)
108            {
109                _logger.error("Failed to retrieve the node ids. Can't clean the reference", e);
110                throw new RepositoryException("Failed to retrieve the node ids. Can't clean the reference", e);
111            }
112        }
113        else
114        {
115            throw new UnsupportedRepositoryOperationException("The repository doesn't support the operation");
116        }
117    }
118    
119    private void _cleanReferences(NodeId nodeId, BundleDbPersistenceManager persistenceManager)
120    {
121        try
122        {
123            NodeReferences nodeRefs = persistenceManager.loadReferencesTo(nodeId);
124            List<PropertyId> refs = nodeRefs.getReferences();
125            final int totalRefs = refs.size();
126            int localInconsistent = 0;
127            
128            for (PropertyId ref: refs)
129            {
130                String uuid = ref.getParentId().toString();
131                try
132                {
133                    _session.getNodeByIdentifier(uuid);
134                }
135                catch (RepositoryException e)
136                {
137                    _logger.debug("Inconsistent reference: " + nodeId.toString() + " <- " + uuid);
138                    localInconsistent++;
139                }
140            }
141            
142            if (localInconsistent > 0)
143            {
144                if (localInconsistent == totalRefs)
145                {
146                    _inconsistent++;
147                    try
148                    {
149                        Node node = _session.getNodeByIdentifier(nodeId.toString());
150                        _logger.debug("Inconsistent references to " + node.getPath() + " (" + nodeId.toString() + ") will be deleted");
151                        persistenceManager.destroy(nodeRefs);
152                        _cleaned++;
153                    }
154                    catch (RepositoryException e)
155                    {
156                        _logger.warn("Failed to retrieve node " + nodeId.toString() + ". "
157                                + "This node had inconsistencies. The node will be skipped and the process will continue.", e);
158                    }
159                    catch (ItemStateException e)
160                    {
161                        _logger.warn("Failed to destroy node references to inconsistent node " + nodeId.toString() + ". "
162                                + "The node will be skipped and the process will continue.", e);
163                    }
164                }
165                else
166                {
167                    _partiallyInconsistent++;
168                    _logger.info("Node " + nodeId.toString() + " is partially inconsistent. "
169                            + "Nothing has been implemented to handle those case. It will be ignored by the cleaning operation.");
170                }
171            }
172            _handled++;
173        }
174        catch (ItemStateException e)
175        {
176            _logger.warn("Failed to retrieve references to node " + nodeId.toString() + ". "
177                    + "The node will be skipped and the process will continue.", e);
178        }
179    }
180
181    @Override
182    protected void close()
183    {
184        _session.logout();
185        
186        super.close();
187    }
188        
189    @Override
190    protected void setLogger()
191    {
192        setLogger(LoggerFactory.getLogger(CleanReferenceTask.class));
193    }
194}
195