001/*
002 *  Copyright 2016 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.workspaces.repository.maintenance;
017
018import java.io.File;
019import java.util.Map;
020import java.util.concurrent.Callable;
021import java.util.concurrent.ExecutorService;
022import java.util.concurrent.Executors;
023import java.util.concurrent.FutureTask;
024
025import javax.jcr.RepositoryException;
026import javax.jcr.UnsupportedRepositoryOperationException;
027import javax.servlet.ServletException;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.logger.AbstractLogEnabled;
033import org.apache.avalon.framework.logger.Logger;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.cocoon.Constants;
038import org.apache.cocoon.environment.Context;
039import org.apache.commons.lang3.tuple.Pair;
040import org.apache.jackrabbit.core.RepositoryContext;
041import org.apache.jackrabbit.core.config.ConfigurationException;
042import org.apache.jackrabbit.core.config.RepositoryConfig;
043
044import org.ametys.core.util.SystemStatus;
045import org.ametys.plugins.repositoryapp.RepositoryProvider;
046import org.ametys.runtime.util.AmetysHomeHelper;
047
048/**
049 * The MaintenanceTaskManager Component
050 */
051public class MaintenanceTaskManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
052{
053    /** The avalon role. */
054    public static final String ROLE = MaintenanceTaskManager.class.getName();
055    
056    /** The repository unavailable system status */
057    public static final String REPOSITORY_UNAVAILABLE = "REPOSITORY_UNAVAILABLE";
058    
059    /** The maintenance task running system status */
060    public static final String MAINTENANCE_TASK_RUNNING = "MAINTENANCE_TASK_RUNNING";
061    
062    /** The maintenance task ended system status */
063    public static final String MAINTENANCE_TASK_ENDED = "MAINTENANCE_TASK_ENDED";
064    
065    /** The maintenance running task */
066    protected static AbstractMaintenanceTask _runningTask;
067    
068    /** The maintenance running task */
069    protected static MaintenanceTaskType _lastRunningTaskType;
070    
071    /** The maintenance running task type */
072    protected MaintenanceTaskType _runningTaskType;
073
074    /** System status provider */
075    protected SystemStatus _systemStatus;
076    
077    /** The repository provider. */
078    protected RepositoryProvider _repositoryProvider;
079    
080    private final ExecutorService _executor = Executors.newFixedThreadPool(1);
081    
082    private FutureTask<Void> _future;
083
084    private boolean _repositoryShutdown;
085
086    private Context _context;
087    
088    /** Task types */
089    public enum MaintenanceTaskType
090    {
091        /** Remove unused history */
092        REMOVE_UNUSED_HISTORY,
093        /** data store GC */
094        DATA_STORE_GARBAGE_COLLECTOR,
095        /** reindexing task */
096        REINDEXING,
097        /** consistency check */
098        CONSISTENCY_CHECK,
099        /** remove inconsistent references */
100        REMOVE_INCOHERENT_REFERENCES,
101        /** reclaim unused space in the derby data store */
102        RECLAIM_UNUSED_SPACE;
103    }
104
105    @Override
106    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
107    {
108        _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
109    }
110    
111    @Override
112    public void service(ServiceManager manager) throws ServiceException
113    {
114        if (manager.hasService(RepositoryProvider.ROLE))
115        {
116            // In safe-mode, the repository provider can not be retrieved
117            _repositoryProvider = (RepositoryProvider) manager.lookup(RepositoryProvider.ROLE);
118        }
119        _systemStatus = (SystemStatus) manager.lookup(SystemStatus.ROLE);
120    }
121
122    /**
123     * Launch a maintenance task
124     * @param type The maintenance task type
125     * @return true if a task has been launched
126     * @throws Exception when an error occurred
127     */
128    public boolean launch(MaintenanceTaskType type) throws Exception
129    {
130        boolean launched = false;
131        if (!isTaskRunning())
132        {
133            try
134            {
135                run(type);
136                launched = true;
137            }
138            catch (ConfigurationException e)
139            {
140                getLogger().error("Unable to get the configuration of the repository.", e);
141                throw e;
142            }
143            catch (RepositoryException e)
144            {
145                getLogger().error("Unable to launch the task.", e);
146                throw e;
147            }
148        }
149        return launched;
150    }
151
152    private void run(final MaintenanceTaskType type) throws ConfigurationException, RepositoryException
153    {
154        _runningTaskType = type;
155        _runningTask = _createTask(type);
156        
157        // add system status
158        if (_runningTask.requiresOffline())
159        {
160            _systemStatus.addStatus(REPOSITORY_UNAVAILABLE);
161        }
162        _systemStatus.addStatus(MAINTENANCE_TASK_RUNNING);
163        _systemStatus.removeStatus(MAINTENANCE_TASK_ENDED);
164        
165        final Logger logger = getLogger();
166        
167        
168        Pair<RepositoryConfig, RepositoryContext> repositoryInfo = _getRepositoryInfo();
169        
170        _future = new FutureTask<>(new Callable<Void>() 
171        {
172            public Void call() throws ServletException
173            {
174                try
175                {
176                    _runningTask.execute(repositoryInfo);
177                }
178                catch (RepositoryException e)
179                {
180                    logger.error(e.getMessage(), e);
181                }
182                finally
183                {
184                    // Currently repository cannot be restarted, the Ametys application have to be reloaded.
185                    // So the REPOSITORY_UNAVAILABLE system status cannot be removed
186                    // _systemStatus.removeStatus(REPOSITORY_UNAVAILABLE);
187                    _systemStatus.removeStatus(MAINTENANCE_TASK_RUNNING);
188                    _systemStatus.addStatus(MAINTENANCE_TASK_ENDED);
189                    _runningTaskType = null;
190                }
191
192                return null;
193            }
194        });
195        
196        _executor.execute(_future);
197    }
198
199    /**
200     * Retrieve the repository config if no repository is available or the repository context of the available repository
201     * @return Either the config or the context depending if the repository is available. The other one will be null.
202     * @throws ConfigurationException if an error occurred while generating the config
203     * @throws UnsupportedRepositoryOperationException if the repository doesn't support retrieving the context
204     */
205    protected Pair<RepositoryConfig, RepositoryContext> _getRepositoryInfo() throws ConfigurationException, UnsupportedRepositoryOperationException
206    {
207        final RepositoryConfig repositoryConfig;
208        final RepositoryContext repositoryContext;
209        
210        if (_repositoryProvider == null)
211        {
212            repositoryConfig = _createRepositoryConfig();
213            repositoryContext = null;
214        }
215        else
216        {
217            _runningTaskType = null;
218            throw new UnsupportedRepositoryOperationException("A maintenance task can not be run on an online repository instance of " + _repositoryProvider.getRepository().getClass().getName());
219        }
220        return Pair.of(repositoryConfig, repositoryContext);
221    }
222    
223    /**
224     * Create a repository config
225     * @return the repository config
226     * @throws ConfigurationException if an error occurred
227     */
228    protected RepositoryConfig _createRepositoryConfig() throws ConfigurationException
229    {
230        String config = _context.getRealPath("/WEB-INF/param/repository.xml");
231        
232        File homeFile = new File(AmetysHomeHelper.getAmetysHomeData(), "repository");
233        
234        if (getLogger().isDebugEnabled())
235        {
236            getLogger().debug("Creating JCR Repository config at: " + homeFile.getAbsolutePath());
237        }
238        
239        return RepositoryConfig.create(config, homeFile.getAbsolutePath());
240    }
241    
242    /**
243     * Initialize the tasks.
244     * @param type MaintenanceTaskType
245     * @return the task.
246     */
247    protected AbstractMaintenanceTask _createTask(MaintenanceTaskType type)
248    {
249        switch (type)
250        {
251            case REMOVE_UNUSED_HISTORY: //online + renforce pour
252                return new RemoveUnusedHistoryTask();
253            case DATA_STORE_GARBAGE_COLLECTOR:
254                return new DataStoreGarbageCollectorTask();
255            case REINDEXING: // offline
256                return new ReindexingTask();
257            case CONSISTENCY_CHECK: // ??
258                return new ConsistencyCheckTask();
259            default:
260                throw new IllegalArgumentException("This type of maintenance task is not allowed : " + type);
261        }
262    }
263
264    /**
265     * Indicates if a maintenance task is running.
266     * @return true if a task is running.
267     */
268    public boolean isTaskRunning()
269    {
270        return _runningTaskType != null;
271    }
272
273    /**
274     * Get progress information.
275     * @return a map containing information on the progress status.
276     */
277    public Map<String, Object> getProgressInfo()
278    {
279        if (_runningTask != null)
280        {
281            return _runningTask.getProgressInfo();
282        }
283
284        return null;
285    }
286
287    /**
288     * Retrieves the type of the running task
289     * @return the type of the running task or null.
290     */
291    public String getRunningTaskType()
292    {
293        if (_runningTaskType != null)
294        {
295            return _runningTaskType.name();
296        }
297
298        return null;
299    }
300    
301    /**
302     * Retrieves the type of the previous running task, if no task is currently running.
303     * Otherwise, the current task type will be return.
304     * @return the type or null
305     */
306    public String getLastRunningTaskType()
307    {
308        if (_lastRunningTaskType != null)
309        {
310            return _lastRunningTaskType.name();
311        }
312        
313        return null;
314    }
315
316    /**
317     * Indicates is the Ametys repository is started or has been shut down.
318     * @return true is the repository is started
319     */
320    public boolean isRepositoryStarted()
321    {
322        return !_repositoryShutdown;
323    }
324}