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