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.io.File;
019import java.sql.SQLException;
020
021import javax.jcr.RepositoryException;
022import javax.sql.DataSource;
023
024import org.apache.commons.io.FileUtils;
025import org.apache.commons.lang.StringUtils;
026import org.apache.jackrabbit.core.RepositoryContext;
027import org.apache.jackrabbit.core.persistence.PersistenceManager;
028import org.apache.jackrabbit.core.persistence.pool.BundleDbPersistenceManager;
029import org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager;
030import org.apache.jackrabbit.core.util.db.ConnectionFactory;
031import org.slf4j.LoggerFactory;
032
033import org.ametys.plugins.repository.provider.AmetysPersistenceManager;
034import org.ametys.workspaces.repository.maintenance.AbstractMaintenanceTask;
035import org.ametys.workspaces.repository.maintenance.DataStoreGarbageCollectorTask;
036import org.ametys.workspaces.repository.maintenance.TaskProgress;
037
038/**
039 * Maintenance Task to reclaim unused space from the Derby tables
040 */
041public class ReclaimUnusedSpaceTask extends AbstractMaintenanceTask
042{
043    @Override
044    protected void apply() throws RepositoryException
045    {
046        _logger.info("Reclaiming unused space, this may take several minutes depending on the size of the data store.\nPlease be patient.");
047        
048        RepositoryContext repositoryContext = getOrCreateRepositoryContext();
049        String[] workspaceNames = repositoryContext.getWorkspaceManager().getWorkspaceNames();
050        
051        _progress = new TaskProgress(workspaceNames.length + 1);
052        
053        for (String workspaceName : workspaceNames)
054        {
055            try
056            {
057                _logger.info("Handling workspace " + workspaceName + "...");
058                PersistenceManager persistenceManager = repositoryContext.getWorkspaceInfo(workspaceName).getPersistenceManager();
059                _reclaimUnusedSpace(persistenceManager);
060            }
061            catch (Exception e)
062            {
063                _logger.warn("An error occurred while handling workspace " + workspaceName + ". The workspace will be skipped.", e);
064            }
065            _progress.progress();
066        }
067        
068        try
069        {
070            _logger.info("Handling version history...");
071            PersistenceManager versionPersistenceManager = repositoryContext.getInternalVersionManager().getPersistenceManager();
072            _reclaimUnusedSpace(versionPersistenceManager);
073        }
074        catch (Exception e)
075        {
076            _logger.warn("An error occurred while handling version data source. The datasource will be skipped.", e);
077        }
078        _progress.progress();
079    }
080
081    private void _reclaimUnusedSpace(PersistenceManager persistenceManager) throws RepositoryException
082    {
083        if (persistenceManager instanceof AmetysPersistenceManager ametysPersistenceManager)
084        {
085            // the AmetysPersistenceManager is a wrapper, we want the actual PM
086            _reclaimUnusedSpace(ametysPersistenceManager.getWrappedPM());
087        }
088        else if (persistenceManager instanceof DerbyPersistenceManager derbyPersistenceManager)
089        {
090            _reclaimUnusedSpace(derbyPersistenceManager);
091        }
092    }
093    
094    private void _reclaimUnusedSpace(DerbyPersistenceManager derbyPersistenceManager) throws RepositoryException
095    {
096        String url = derbyPersistenceManager.getUrl();
097        String path = StringUtils.substringAfter(url, "jdbc:derby:");
098        path = StringUtils.substringBefore(path, ";");
099        File dbDirectory = new File(path);
100        
101        DataSource dataSource = _getDataSource(derbyPersistenceManager);
102        String schemaObjectPrefix = derbyPersistenceManager.getSchemaObjectPrefix();
103        long startSize = FileUtils.sizeOfDirectory(dbDirectory);
104        
105        DataStoreGarbageCollectorTask.derbyCompressTable(dataSource, schemaObjectPrefix + "REFS", _logger);
106        DataStoreGarbageCollectorTask.derbyCompressTable(dataSource, schemaObjectPrefix + "BUNDLE", _logger);
107        
108        long endSize = FileUtils.sizeOfDirectory(dbDirectory);
109        
110        if (startSize > endSize)
111        {
112            _logger.info(String.format("%s freed. (%s -> %s)", FileUtils.byteCountToDisplaySize(startSize - endSize), FileUtils.byteCountToDisplaySize(startSize), FileUtils.byteCountToDisplaySize(endSize)));
113        }
114        else
115        {
116            _logger.info("No space freed");
117        }
118    }
119
120    /** 
121     * Try to retrieve the data source from a persistence manager.
122     * The data source will be retrieve either by its name or using its URL
123     * @param pm the persistence manager
124     * @return the data source
125     */
126    private DataSource _getDataSource(BundleDbPersistenceManager pm) throws RepositoryException
127    {
128        try
129        {
130            ConnectionFactory cf = getRepositoryConfig().getConnectionFactory();
131            if (pm.getDataSourceName() == null || "".equals(pm.getDataSourceName()))
132            {
133                return cf.getDataSource(pm.getDriver(), pm.getUrl(), pm.getUser(), pm.getPassword());
134            }
135            else
136            {
137                return cf.getDataSource(pm.getDataSourceName());
138            }
139        }
140        catch (SQLException e)
141        {
142            _logger.error(e.getLocalizedMessage(), e);
143            throw new RuntimeException(e);
144        }
145        catch (RepositoryException e)
146        {
147            _logger.warn("Failed to retrieve the datasource from persistence manager.", e);
148            throw e;
149        }
150        
151    }
152
153    @Override
154    protected void setLogger()
155    {
156        setLogger(LoggerFactory.getLogger(ReclaimUnusedSpaceTask.class));
157    }
158}