001/*
002 *  Copyright 2025 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.repositoryapp.jcr;
017
018import java.util.Map;
019import java.util.Map.Entry;
020
021import javax.jcr.RepositoryException;
022import javax.jcr.Session;
023import javax.jcr.version.Version;
024import javax.jcr.version.VersionHistory;
025import javax.jcr.version.VersionIterator;
026
027import org.apache.jackrabbit.JcrConstants;
028import org.apache.jackrabbit.core.NodeImpl;
029import org.slf4j.Logger;
030
031/**
032 * Helper to manipulate the JCR version history
033 */
034public final class VersionHistoryHelper
035{
036    /**
037     * Result of a remove version history operation
038     */
039    public enum RemoveHistoryResult
040    {
041        /** The version history has been removed */
042        REMOVED,
043        /** The version history is still referenced and could not be removed */
044        REFERENCED,
045        /** An error occurred preventing the removal */
046        ERROR,
047        /** The version history was broken and has been removed */
048        FIXED,
049        /** The version history is broken but could not be removed */
050        UNFIXABLE
051    }
052    
053    private VersionHistoryHelper()
054    {
055        // Utility class
056    }
057    
058    /**
059     * Remove the version history if it is unused (ie the versionable node does not exist anymore)
060     * @param sessions the sessions used to determine if the versionable exist. Multiple session can be provided to search in multiple workspace for example.
061     * @param versionHistory the version history node to remove
062     * @param logger a logger to use to provide detailed information
063     * @return the result of the operation
064     * @throws RepositoryException if an error occurs
065     */
066    public static RemoveHistoryResult removeUnusedHistory(Map<String, Session> sessions, VersionHistory versionHistory, Logger logger) throws RepositoryException
067    {
068        String versionableIdentifier = versionHistory.getVersionableIdentifier();
069        
070        // check that the history is actually unused
071        for (Entry<String, Session> entry: sessions.entrySet())
072        {
073            try
074            {
075                entry.getValue().getNodeByIdentifier(versionableIdentifier);
076                return RemoveHistoryResult.REFERENCED;
077            }
078            catch (RepositoryException e)
079            {
080                // nothing
081            }
082        }
083        
084        // unused node, we may remove associated version history
085        VersionIterator it = versionHistory.getAllVersions();
086        
087        int childNodesSize = 0;
088        
089        while (it.hasNext())
090        {
091            Version version = it.nextVersion();
092            if (!JcrConstants.JCR_ROOTVERSION.equals(version.getName()))
093            {
094                childNodesSize++;
095                
096                try
097                {
098                    versionHistory.removeVersion(version.getName());
099                }
100                catch (RepositoryException e)
101                {
102                    logger.error("Error with version " + version.getName() + " of " + versionHistory.getIdentifier() + "... " + e);
103                    return RemoveHistoryResult.ERROR;
104                }
105            }
106        }
107        
108        if (childNodesSize == 0)
109        {
110            _removeEmptyHistory(versionHistory, sessions.get("default"));
111            try
112            {
113                sessions.get("default").getNodeByIdentifier(versionHistory.getIdentifier());
114                
115                logger.debug("Empty history node is " + versionHistory.getIdentifier());
116                return RemoveHistoryResult.UNFIXABLE;
117            }
118            catch (RepositoryException e)
119            {
120                return RemoveHistoryResult.FIXED;
121            }
122        }
123        else
124        {
125            return RemoveHistoryResult.REMOVED;
126        }
127    }
128    
129    private static void _removeEmptyHistory(VersionHistory vh, Session session) throws RepositoryException
130    {
131        var node = ((NodeImpl) session.getNode("/ametys:root")).addNodeWithUuid("historyrepair", JcrConstants.NT_UNSTRUCTURED, vh.getProperty(JcrConstants.JCR_VERSIONABLEUUID).getString());
132        node.addMixin("mix:versionable");
133        session.save();
134        session.getWorkspace().getVersionManager().checkin(node.getPath());
135        session.getWorkspace().getVersionManager().checkout(node.getPath());
136        session.removeItem("/ametys:root/historyrepair");
137        session.save();
138        vh.removeVersion("1.0");
139    }
140
141}