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