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