Index: main/workspace-repository/i18n/messages_en.xml =================================================================== --- main/workspace-repository/i18n/messages_en.xml (revision 19543) +++ main/workspace-repository/i18n/messages_en.xml (working copy) @@ -134,6 +134,23 @@ Properties + + + Maintenance + Maintenance hints<br/>Aide sur la maintenance (TODO) + This tab allows you to launch automated maintenance task. Three tasks are available :<ul><li>- Data store garbage collection</li><li>- Re-indexing</li><li>- Consistency check</li>Click on the corresponding task button to launch it. A progress bar and a report will be displayed to easily track the task progress. + Progress + % + Garbage collector + Re-indexing + Consistency check + + An error occurred. + Unable to get response from the server. Cannot get informations if a task is running. + Unable to get response from the server. The launch of the task has probably failed. + Task failed to launch. + Perhaps a task is currently running. Try reloading the page. + Unable to get response from the server. The tracking of the task progress is currently not up to date. Console Index: main/workspace-repository/i18n/messages_fr.xml =================================================================== --- main/workspace-repository/i18n/messages_fr.xml (revision 19543) +++ main/workspace-repository/i18n/messages_fr.xml (working copy) @@ -134,6 +134,22 @@ Propriétés + + + Maintenance + Cet onglet permet de lancer des tâches de maintenance automatiques. Les tâches suivantes sont disponibles :<ul><li>- Ramasse miettes</li><li>- Ré-indexation</li><li>- Vérification de la cohérence</li>Veuillez cliquez sur le bouton correspondant à la tâche de votre choix pour lancer celle-ci. Une barre de progression, ainsi qu'un rapport s'affichera pour vous permettre de suivre l'avancement du processus. + Avancement + % + Ramasse miettes + Ré-indexation + Vérification de la cohérence + + Une erreur s'est produite. + La récupération des informations serveur a échouée. Impossible de déterminer si une tâche de maintenance est déjà en cours. + Impossible de récupérer les informations serveur. Le lancement de la tâche de maintenance a probablement échoué. + Le lancement de la tâche de maintenance a échoué. + Il est possible qu'une tâche soit déjà en cours d'éxecution. Veuillez essayer de recharger la page. + Impossible de récupérer les informations serveur. Les informations relative à la progression de la tâche et le panneau de rapport ne sont plus à jour pour l'instant. Console Index: main/workspace-repository/pages/index.xsl =================================================================== --- main/workspace-repository/pages/index.xsl (revision 19543) +++ main/workspace-repository/pages/index.xsl (working copy) @@ -50,6 +50,7 @@ + Index: main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantIsRunningTaskGenerator.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantIsRunningTaskGenerator.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantIsRunningTaskGenerator.java (revision 0) @@ -0,0 +1,60 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.workspaces.repository; + +import java.io.IOException; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.generation.ServiceableGenerator; +import org.apache.cocoon.xml.AttributesImpl; +import org.apache.cocoon.xml.XMLUtils; +import org.apache.commons.lang.StringUtils; +import org.xml.sax.SAXException; + +import org.ametys.workspaces.repository.tasks.MaintenanceTaskManager; + +/** + * Tells if a task is running. + */ +public class MaintenantIsRunningTaskGenerator extends ServiceableGenerator +{ + /** The maintenance task manager */ + private MaintenanceTaskManager _taskManager; + + @Override + public void service(ServiceManager m) throws ServiceException + { + super.service(m); + _taskManager = (MaintenanceTaskManager) m.lookup(MaintenanceTaskManager.ROLE); + } + + public void generate() throws IOException, SAXException, ProcessingException + { + contentHandler.startDocument(); + + String task = _taskManager.getRunningTaskType(); + String isRunning = Boolean.toString(_taskManager.isTaskRunning()); + + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("running", isRunning); + attrs.addCDATAAttribute("repository-state", Boolean.toString(_taskManager.isRepositoryStarted())); + XMLUtils.createElement(contentHandler, "task", attrs, task != null ? task : StringUtils.EMPTY); + + contentHandler.endDocument(); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantLaunchTaskGenerator.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantLaunchTaskGenerator.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/MaintenantLaunchTaskGenerator.java (revision 0) @@ -0,0 +1,63 @@ +/* + * Copyright 2010 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.workspaces.repository; + +import java.io.IOException; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Request; +import org.apache.cocoon.generation.ServiceableGenerator; +import org.apache.cocoon.xml.AttributesImpl; +import org.apache.cocoon.xml.XMLUtils; +import org.xml.sax.SAXException; + +import org.ametys.workspaces.repository.tasks.MaintenanceTaskManager; + +/** + * Generated information about the task launching process. + */ +public class MaintenantLaunchTaskGenerator extends ServiceableGenerator +{ + /** The maintenance task manager */ + private MaintenanceTaskManager _taskManager; + + @Override + public void service(ServiceManager m) throws ServiceException + { + super.service(m); + _taskManager = (MaintenanceTaskManager) m.lookup(MaintenanceTaskManager.ROLE); + } + + public void generate() throws IOException, SAXException, ProcessingException + { + Request request = ObjectModelHelper.getRequest(objectModel); + + String task = request.getParameter("task"); + Boolean launched = parameters.getParameterAsBoolean("launched", false); + + contentHandler.startDocument(); + + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("launched", Boolean.toString(launched)); + attrs.addCDATAAttribute("repository-state", Boolean.toString(_taskManager.isRepositoryStarted())); + XMLUtils.createElement(contentHandler, "task", attrs, task); + + contentHandler.endDocument(); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/MaintenanceTaskManager.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/MaintenanceTaskManager.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/MaintenanceTaskManager.java (revision 0) @@ -0,0 +1,273 @@ +package org.ametys.workspaces.repository.tasks; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +import javax.jcr.RepositoryException; + +import org.apache.avalon.framework.component.Component; +import org.apache.avalon.framework.context.ContextException; +import org.apache.avalon.framework.context.Contextualizable; +import org.apache.avalon.framework.logger.AbstractLogEnabled; +import org.apache.avalon.framework.logger.Logger; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.avalon.framework.service.Serviceable; +import org.apache.cocoon.Constants; +import org.apache.cocoon.environment.Context; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +import org.ametys.plugins.repositoryapp.RepositoryProvider; +import org.ametys.runtime.config.Config; +import org.ametys.workspaces.repository.tasks.core.AbstractMaintenanceTask; +import org.ametys.workspaces.repository.tasks.core.TaskReport.TaskReportMessage; + +/** + * The MaintenanceTaskManager Component + */ +public class MaintenanceTaskManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable +{ + /** The avalon role. */ + public static final String ROLE = MaintenanceTaskManager.class.getName(); + + /** The maintenance running task */ + protected AbstractMaintenanceTask _runningTask; + + /** The maintenance running task type */ + protected MaintenanceTaskType _runningTaskType; + + private Context _context; + + /** The repository provider. */ + private RepositoryProvider _repositoryProvider; + + private final ExecutorService _executor = Executors.newFixedThreadPool(1); + private FutureTask _future; + + private boolean _repositoryShutdown; + + /** Task types */ + public enum MaintenanceTaskType + { + /** data store GC */ + DATA_STORE_GARBAGE_COLLECTOR, + /** reindexing task */ + REINDEXING, + /** consistency check */ + CONSISTENCY_CHECK; + } + + @Override + public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException + { + _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); + } + + @Override + public void service(ServiceManager manager) throws ServiceException + { + _repositoryProvider = (RepositoryProvider) manager.lookup(RepositoryProvider.ROLE); + } + + /** + * Launch a maintenance task + * @param type + * @return true if a task has been launched + */ + public boolean launch(MaintenanceTaskType type) + { + boolean launched = false; + if (!isTaskRunning()) + { + try + { + run(type); + launched = true; + } + catch (ConfigurationException e) + { + getLogger().error("Unable to get the configuration of the repository.", e); + } + catch (RepositoryException e) + { + getLogger().error("Unable to launch the task.", e); + } + } + return launched; + } + + private void run(final MaintenanceTaskType type) throws ConfigurationException, RepositoryException + { + _runningTaskType = type; + + // Shutdown repository + shutdownRepository(); + + final Logger logger = getLogger(); + final RepositoryConfig repositoryConfig = _getRepositoryConfig(); + _future = new FutureTask( + new Callable() + { + public Void call() + { + try + { + _runningTask = _createTask(type); + _runningTask.execute(repositoryConfig); + } + catch (RepositoryException e) + { + logger.error(e.getMessage(), e); + } + finally + { + restartRepository(); + _runningTaskType = null; + } + + return null; + } + }); + + _executor.execute(_future); + } + + /** + * Retrieve the repository config object + * @return RepositoryConfig + * @throws ConfigurationException + */ + private RepositoryConfig _getRepositoryConfig() throws ConfigurationException + { + String home = Config.getInstance().getValueAsString("org.ametys.plugins.repository.home"); + String config = _context.getRealPath("WEB-INF/param/repository.xml"); + + File homeFile = new File(home); + if (!homeFile.isAbsolute()) + { + // No : consider it relative to context path + homeFile = new File(_context.getRealPath(home)); + } + + return RepositoryConfig.create(config, homeFile.getAbsolutePath()); + } + + /** + * Shutdown the Ametys repository instance + * @throws RepositoryException + */ + protected void shutdownRepository() throws RepositoryException + { + if (_repositoryProvider.isJndi()) + { + throw new RepositoryException("JNDI Repository instance are currently not allowed."); + } + + // FIXME Shutdown the repository + _repositoryProvider.disconnect(); + _repositoryShutdown = true; + + if (getLogger().isInfoEnabled()) + { + getLogger().info("Repository instance has been shutdown in order to run an automated maintenance task."); + } + } + + /** + * Re-create the Ametys repository instance + */ + protected void restartRepository() + { + // FIXME @see #shutdownRepository() + // _repositoryShutdown = true; + } + + /** + * Initialize the tasks. + * @param type MaintenanceTaskType + * @return the task. + * @throws RepositoryException + */ + protected AbstractMaintenanceTask _createTask(MaintenanceTaskType type) throws RepositoryException + { + switch(type) + { + case DATA_STORE_GARBAGE_COLLECTOR: + return new DataStoreGarbageCollectorTask(); + case REINDEXING: + return new ReindexingTask(); + case CONSISTENCY_CHECK: + return new ConsistencyCheckTask(); + default: + throw new IllegalArgumentException("This type of maintenance task is not allowed : " + type); + } + } + + /** + * Indicates if a maintenance task is running. + * @return true if a task is running. + */ + public boolean isTaskRunning() + { + return _runningTaskType != null; + } + + /** + * Get progress information. + * @return a map containing informations on the progress status. + */ + public Map getProgressInfo() + { + if (_runningTask != null) + { + return _runningTask.getProgressInfo(); + } + + return null; + } + + /** + * Get the {@link TaskReportMessage} objects starting from a given part. + * @param list the list of {@link TaskReportMessage} to be populated + * @param fromPart + * @return the last report part number + */ + public Integer getReportMessage(List list, int fromPart) + { + if (_runningTask != null) + { + return _runningTask.getReportMessage(list, fromPart); + } + + return null; + } + + /** + * Retrieves the type of the running task + * @return the type of the running task or null. + */ + public String getRunningTaskType() + { + if (_runningTaskType != null) + { + return _runningTaskType.name(); + } + + return null; + } + + /** + * Indicates is the Ametys repository is started or has been shut down. + * @return true is the repository is started + */ + public boolean isRepositoryStarted() + { + return !_repositoryShutdown; + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ConsistencyCheckTask.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ConsistencyCheckTask.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ConsistencyCheckTask.java (revision 0) @@ -0,0 +1,188 @@ +package org.ametys.workspaces.repository.tasks; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; +import org.apache.jackrabbit.core.persistence.check.ConsistencyCheckListener; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.core.persistence.check.ReportItem; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; + +import org.ametys.workspaces.repository.tasks.core.AbstractMaintenanceTask; +import org.ametys.workspaces.repository.tasks.core.TaskProgress; +import org.ametys.workspaces.repository.tasks.core.TaskReport.TaskReportMessageType; + +/** + * DataStoreGarbageCollectorTask + */ +public class ConsistencyCheckTask extends AbstractMaintenanceTask implements ConsistencyCheckListener +{ + /** The JackRabbit RepositoryImpl Context */ + protected RepositoryContext _repositoryContext; + + /** The JCR Session bound to this task. */ + protected Session _session; + + private IterablePersistenceManager[] _pmList; + + @Override + protected void initialize() throws RepositoryException + { + // Create the repository and log in the session. + _repositoryContext = RepositoryContext.create(_repositoryConfig); + _session = _repositoryContext.getRepository().login(new SimpleCredentials("__MAINTENANCE_TASK__", "".toCharArray())); + + // Workaround to get the list of the PersistenceManager + ArrayList pmList = new ArrayList(); + + // PM of version manager + InternalVersionManagerImpl vm = _repositoryContext.getInternalVersionManager(); + pmList.add(vm.getPersistenceManager()); + + // PMs of workspaces. + String[] wspNames = _repositoryContext.getWorkspaceManager().getWorkspaceNames(); + for (int i = 0; i < wspNames.length; i++) + { + pmList.add(getPM(wspNames[i])); + } + + // Filtering on IterablePersistenceManager + _pmList = new IterablePersistenceManager[pmList.size()]; + for (int i = 0; i < pmList.size(); i++) + { + PersistenceManager pm = pmList.get(i); + if (!(pm instanceof IterablePersistenceManager)) + { + _pmList = null; + break; + } + _pmList[i] = (IterablePersistenceManager) pm; + } + + // Initialize the task progress object. + int count = 0; // number of item that will be scanned. + + try + { + for (IterablePersistenceManager pm : _pmList) + { + count += pm.getAllNodeIds(null, 0).size(); + } + _progress = new TaskProgress(count); + } + catch (Exception e) + { + _progress = new TaskProgress(0); + _progress.setInErrorState(e); + _report.addException(e); + } + } + + @Override + protected void apply() throws RepositoryException + { + for (PersistenceManager pm : _pmList) + { + // Do PM consistency check + if (pm instanceof AbstractBundlePersistenceManager) + { + // Perform the check + // null -> all uuids, true -> recursive, false -> nofix, null, + // lost+found -> null, this -> listener + ConsistencyReport report = ((AbstractBundlePersistenceManager) pm).check(null, true, false, null, this); + + _report.add("Consistency check done for persistence manager : '" + pm.toString() + "' in " + (report.getElapsedTimeMs() / 1000f) + " s."); + _report.add(report.getNodeCount() + " nodes were checked."); + _report.add(report.getItems().isEmpty() ? "No consistency problems where reported." : report.getItems().size() + " consistency problems where reported."); + } + } + } + + @Override + protected void close() + { + if (_session != null) + { + _session.logout(); + } + + if (_repositoryContext != null && _repositoryContext.getRepository() != null) + { + _repositoryContext.getRepository().shutdown(); + } + } + + /** + * Retrieves JackRabbit Persistence Manager for currently opened repository. This method uses + * Privileged access and will fail with security exception if used in environment with enabled security manager. + * + * @return Persistence manager used by repository. + */ + private PersistenceManager getPM(String workspaceName) + { + try + { + Object workspaceInfo = findAndInvokeMethod(_repositoryContext.getRepository(), "getWorkspaceInfo", new Object[] {workspaceName}); + return (PersistenceManager) (findAndInvokeMethod(workspaceInfo, "getPersistenceManager", null)); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + private static Object findAndInvokeMethod(Object obj, String name, Object[] parameters) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + Method m = null; + Method[] ms = obj.getClass().getDeclaredMethods(); + for (int i = 0; i < ms.length; i++) + { + final Method x = ms[i]; + if (x.getName().equals(name)) + { + m = x; + break; + } + } + m.setAccessible(true); + return m.invoke(obj, parameters); + } + + // Listener methods + + @Override + public void startCheck(String id) + { + if (_progress != null) + { + _progress.progress(); + } + } + + @Override + public void report(ReportItem item) + { + _report.add(TaskReportMessageType.WARN, item.toString()); + } + + @Override + public void error(String id, String message) + { + _report.add(TaskReportMessageType.ERROR, "error during the consistency check -> id : [ " + id + "]\n" + message); + } + + @Override + public void info(String id, String message) + { + _report.add("error during the consistency check -> id : [ " + id + "]\n" + message); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/AbstractMaintenanceTask.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/AbstractMaintenanceTask.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/AbstractMaintenanceTask.java (revision 0) @@ -0,0 +1,190 @@ +package org.ametys.workspaces.repository.tasks.core; + +import java.text.DateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.joda.time.Duration; +import org.joda.time.format.PeriodFormatter; +import org.joda.time.format.PeriodFormatterBuilder; + +import org.ametys.plugins.repositoryapp.RepositoryProvider; +import org.ametys.workspaces.repository.tasks.core.TaskReport.TaskReportMessage; + +/** + * Jackrabbit maintenance tasks implementations should extends this class. + */ +public abstract class AbstractMaintenanceTask +{ + /** TaskProgress */ + protected TaskProgress _progress; + + /** task report */ + protected TaskReport _report; + + /** The repository config */ + protected RepositoryConfig _repositoryConfig; + + /** The repository provider. */ + protected RepositoryProvider _repositoryProvider; + + private boolean _isFinished; + + /** + * Execute the task + * @param repositoryConfig The RepositoryConfig object, used to create new Repository instance. + * @throws RepositoryException + */ + public void execute(RepositoryConfig repositoryConfig) throws RepositoryException + { + _report = new TaskReport(); + _repositoryConfig = repositoryConfig; + + long startTime = System.currentTimeMillis(); + + _report.add(DateFormat.getTimeInstance().format(new Date()) + " : Executing task"); + + try + { + // task specific initialization. + initialize(); + + // Mark the task as running. + setRunning(); + + // Do the task. + apply(); + + // Mark the task as finished. + setFinished(); + } + catch (RepositoryException e) + { + setInErrorState(e); + throw e; + } + finally + { + close(); + + _report.add(DateFormat.getTimeInstance().format(new Date()) + " : End of the task"); + + long elapsedTime = System.currentTimeMillis() - startTime; + _report.add("Done in " + _getFormattedDuration(elapsedTime)); + } + } + + /** + * Initialize the tasks. + * This method can also create the {@link TaskProgress} object bounded to the task. + * @throws RepositoryException + */ + protected void initialize() throws RepositoryException + { + return; + } + + /** + * Apply the tasks (within the execute method()). + * @throws RepositoryException + */ + protected abstract void apply() throws RepositoryException; + + /** + * Close the tasks + * @throws RepositoryException + */ + protected void close() throws RepositoryException + { + return; + } + + /** + * Get progress information. + * @return a map containing informations on the progress status. + */ + public Map getProgressInfo() + { + if (_progress != null) + { + return _progress.getProgressInfo(); + } + else + { + return null; + } + } + + /** + * Get the {@link TaskReportMessage} objects starting from a given part. + * @param list the list of {@link TaskReportMessage} to be populated + * @param fromPart + * @return the last report part number + */ + public int getReportMessage(List list, int fromPart) + { + return _report.getMessages(list, fromPart); + } + + /** + * Is the execution of the task finished? + * @return true if finished + */ + public boolean isFinished() + { + return _isFinished; + } + + private synchronized void setRunning() + { + if (_progress != null) + { + _progress.setRunning(); + } + } + + private synchronized void setFinished() + { + if (_progress != null) + { + _progress.setFinished(); + } + + _isFinished = true; + } + + private synchronized void setInErrorState(Exception e) + { + _report.addException(e); + + if (_progress != null) + { + _progress.setInErrorState(e); + } + + _isFinished = true; + } + + /** + * Transforms millisecond to a formatted string representing a duration. + * @param elapsedTime milleseconds corresponding to the duration. + * @return Pretty formatted string. + */ + protected String _getFormattedDuration(long elapsedTime) + { + Duration duration = new Duration(elapsedTime); + PeriodFormatter formatter = new PeriodFormatterBuilder() + .appendHours() + .appendSuffix("h") + .appendMinutes() + .appendSuffix("m") + .appendSeconds() + .appendSuffix("s") + .toFormatter(); + return formatter.print(duration.toPeriod()); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskProgress.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskProgress.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskProgress.java (revision 0) @@ -0,0 +1,249 @@ +package org.ametys.workspaces.repository.tasks.core; + +import java.util.HashMap; +import java.util.Map; + +/** + * TaskProgress + */ +public class TaskProgress +{ + private final float _total; + private float _progress; + private State _state; + + private Exception _exception; + + /** + * Progress state + */ + public enum State + { + /** NEW */ + NEW, + /** RUNNING */ + RUNNING, + /** FINISHED */ + FINISHED, + /** ERROR_STATE */ + ERROR_STATE + } + + /** + * Ctor + * @param total + */ + public TaskProgress(float total) + { + this._total = total; + this._progress = 0f; + this._state = State.NEW; + } + + /** + * Overriden Ctor + * @param total + * @param start + */ + public TaskProgress(float total, float start) + { + this(total); + this._progress = start; + } + + /** + * Unit progress + */ + public void progress() + { + progress(1); + } + + /** + * Quantitative progress + * @param quantity + */ + public synchronized void progress(float quantity) + { + _progress += quantity; + } + + /** + * Progress by a percentage relative to the range [current progress; total] + * @param percentage must be in the [0; 100] range + */ + public void progressRelativePercentage(int percentage) + { + if (percentage < 0 || percentage > 100) + { + throw new IllegalArgumentException("percentage must be in the [0; 100] range."); + } + + synchronized (this) + { + if (percentage == 100) + { + _progress = _total; + } + else + { + _progress += (_total - _progress) * percentage / 100f; + } + } + } + + /** + * Progress by a given percentage + * @param percentage must be in the [0; 100] range + */ + public void progressTotalPercentage(int percentage) + { + if (percentage < 0 || percentage > 100) + { + throw new IllegalArgumentException("percentage must be in the [0; 100] range."); + } + + synchronized (this) + { + if (percentage == 100) + { + _progress = _total; + } + else + { + _progress = _total * percentage / 100f; + } + } + } + + /** + * getProgressPercentage + * @return int + */ + public synchronized float getProgressPercentage() + { + return Math.min(1f, _progress / _total); + } + + /** + * setRunning + */ + public synchronized void setRunning() + { + _state = State.RUNNING; + } + + /** + * setFinished + */ + public synchronized void setFinished() + { + _state = State.FINISHED; + } + + /** + * setInErrorState + */ + public void setInErrorState() + { + setInErrorState(null); + } + + /** + * setInErrorState + * @param e + */ + public synchronized void setInErrorState(Exception e) + { + _exception = e; + _state = State.ERROR_STATE; + } + + /** + * isNotStarted + * @return boolean + */ + public boolean isNotStarted() + { + return State.NEW.equals(_state); + } + + /** + * isRunning + * @return boolean + */ + public boolean isRunning() + { + return State.RUNNING.equals(_state); + } + + /** + * isFinished + * @return boolean + */ + public boolean isFinished() + { + return State.FINISHED.equals(_state); + } + + /** + * isInErrorState + * @return boolean + */ + public boolean isInErrorState() + { + return State.ERROR_STATE.equals(_state); + } + + /** + * getException + * @return Exception + */ + public Exception getException() + { + return _exception; + } + + /** + * getState + * @return State + */ + public State getState() + { + return _state; + } + + + /** + * Returns informations this progress object. + * @return a map containing informations on the progress status + */ + public Map getProgressInfo() + { + Map infos = new HashMap(); + synchronized (this) + { + infos.put("progress", String.valueOf(_progress)); + infos.put("total", String.valueOf(_total)); + infos.put("percentage", String.valueOf(getProgressPercentage())); + infos.put("state", _state.name()); + if (_exception != null) + { + infos.put("exception", _exception.getLocalizedMessage()); + } + } + return infos; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("Progress : "); + synchronized (this) + { + sb.append(getProgressPercentage()).append(" "); + sb.append("[").append(_progress).append("/").append(_total).append("]"); + } + return sb.toString(); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskReport.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskReport.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/core/TaskReport.java (revision 0) @@ -0,0 +1,137 @@ +package org.ametys.workspaces.repository.tasks.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * TaskReport + */ +public class TaskReport +{ + private final List> _reportList = new ArrayList>(); + + private final List _currentList = new ArrayList(); + + /** Task Report message type */ + public enum TaskReportMessageType + { + /** INFO */ + INFO, + /** WARN */ + WARN, + /** ERROR */ + ERROR; + } + + /** + * Add an INFO message to the report + * + * @param msg + */ + public void add(String msg) + { + add(null, msg); + } + + /** + * Add an ERROR message to the report, containing the message of the + * exception. + * + * @param e the exception. + */ + public void addException(Exception e) + { + add(TaskReportMessageType.ERROR, e.getLocalizedMessage()); + } + + /** + * An an message of the specified type to the report + * + * @param type The message type + * @param msg + */ + public void add(TaskReportMessageType type, String msg) + { + TaskReportMessage trm = new TaskReportMessage(type, msg); + synchronized (this) + { + _currentList.add(trm); + } + } + + /** + * Get the {@link TaskReportMessage} objects starting from a given part. + * @param list the list of {@link TaskReportMessage} to be populated + * @param fromPart + * @return the last report part number + */ + public synchronized int getMessages(List list, int fromPart) + { + manageReportList(); + List> subReportList = _reportList.subList(fromPart, _reportList.size()); + for (List subReportListPart : subReportList) + { + list.addAll(subReportListPart); + } + + return _reportList.size(); + } + + private synchronized void manageReportList() + { + if (_currentList.isEmpty()) + { + return; + } + + _reportList.add(new ArrayList(_currentList)); + _currentList.clear(); + } + + /** + * Inner TaskReportMessage class + */ + public class TaskReportMessage + { + private TaskReportMessageType _type; + private String _message; + + /** + * Ctor + * + * @param type + * @param message + */ + public TaskReportMessage(TaskReportMessageType type, String message) + { + _type = type == null ? TaskReportMessageType.INFO : type; + _message = message; + } + + /** + * Retrieves the type of the report message + * @return the type of the report message + */ + public TaskReportMessageType getTaskReportMessageType() + { + return _type; + } + + /** + * Retrieves the content of the report message + * @return the message + */ + public String getMessage() + { + return _message; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(_type).append(" ").append(_message); + return sb.toString(); + } + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ReindexingTask.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ReindexingTask.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/ReindexingTask.java (revision 0) @@ -0,0 +1,116 @@ +package org.ametys.workspaces.repository.tasks; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; + +import org.ametys.workspaces.repository.tasks.core.AbstractMaintenanceTask; +import org.ametys.workspaces.repository.tasks.core.TaskProgress; + +/** + * ReindexingTask + */ +public class ReindexingTask extends AbstractMaintenanceTask +{ + private static final String _INDEX_FOLDER_RELATIVE_PATH = "repository" + File.separator + "index"; + private static final String _WORKSPACES_FOLDER_RELATIVE_PATH = "workspaces"; + + /** The JackRabbit RepositoryImpl */ + protected RepositoryImpl _repository; + + private String[] _workspaceFolders; + + @Override + protected void initialize() throws RepositoryException + { + File file = new File(_repositoryConfig.getHomeDir() + File.separator + _WORKSPACES_FOLDER_RELATIVE_PATH); + _workspaceFolders = file.list(new FilenameFilter() + { + @Override + public boolean accept(File dir, String name) + { + return dir.isDirectory(); + } + }); + + + // Initialize the task progress object. + // deleting workspace index folders + repository root index folder = 60% + // reindexing = 40% + _progress = new TaskProgress(5f / 3 * (_workspaceFolders.length + 1)); + } + + @Override + protected void apply() throws RepositoryException + { + // Deleting repository root index folder + try + { + deleteIndexFolder(_INDEX_FOLDER_RELATIVE_PATH); + _report.add("Successfully deleted root repository index folder"); + } + catch (IOException e) + { + _report.addException(e); + } + + // Deleting workspace index folders + for (String folder : _workspaceFolders) + { + try + { + deleteIndexFolder(_WORKSPACES_FOLDER_RELATIVE_PATH + File.separator + folder + File.separator + "index"); + _report.add("Successfully deleted index folder of workspace '" + folder + "'"); + } + catch (IOException e) + { + _report.addException(e); + } + } + + // Creates a repository instance to perform the re-indexing process. + _report.add("Starting repository to launch the re-indexing process"); + _repository = RepositoryImpl.create(_repositoryConfig); + _report.add("Repository restarted successfully, reindexing process has ended."); + setProgressTo(90); + + // logout + _report.add("Shuting down repository"); + _repository.shutdown(); + _repository = null; + setProgressTo(100); + } + + @Override + protected void close() + { + // shutdown properly if an exception has been thrown + if (_repository != null) + { + _repository.shutdown(); + } + } + + private void deleteIndexFolder(String relativePath) throws IOException + { + File dir = new File(_repositoryConfig.getHomeDir() + File.separator + relativePath); + FileUtils.deleteDirectory(dir); + if (_progress != null) + { + _progress.progress(); + } + } + + private void setProgressTo(int percentage) + { + if (_progress != null) + { + _progress.progressTotalPercentage(percentage); + } + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/tasks/DataStoreGarbageCollectorTask.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/tasks/DataStoreGarbageCollectorTask.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/tasks/DataStoreGarbageCollectorTask.java (revision 0) @@ -0,0 +1,329 @@ +package org.ametys.workspaces.repository.tasks; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.sql.DataSource; + +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.data.FileDataStore; +import org.apache.jackrabbit.core.data.GarbageCollector; +import org.apache.jackrabbit.core.data.db.DbDataStore; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; + +import org.ametys.workspaces.repository.tasks.core.AbstractMaintenanceTask; +import org.ametys.workspaces.repository.tasks.core.TaskProgress; +import org.ametys.workspaces.repository.tasks.core.TaskReport.TaskReportMessageType; + +/** + * DataStoreGarbageCollectorTask + */ +public class DataStoreGarbageCollectorTask extends AbstractMaintenanceTask implements MarkEventListener +{ + private static final int SYSTEM_GC_CALLS = 3; + + /** The JackRabbit RepositoryImpl Context */ + protected RepositoryContext _repositoryContext; + + /** The JCR Session bound to this task. */ + protected Session _session; + + private GarbageCollector _garbageCollector; + private IterablePersistenceManager[] _pmList; + + /** Internal counter for scanned node by the GC */ + private int _scannedNodesCount; + + @Override + protected void initialize() throws RepositoryException + { + // Create the repository and log in the session. + _repositoryContext = RepositoryContext.create(_repositoryConfig); + _session = _repositoryContext.getRepository().login(new SimpleCredentials("__MAINTENANCE_TASK__", "".toCharArray())); + + // Create the GC Object + this._garbageCollector = _repositoryContext.getRepository().createDataStoreGarbageCollector(); + + // Workaround to get the list of the PersistenceManager + ArrayList pmList = new ArrayList(); + InternalVersionManagerImpl vm = _repositoryContext.getInternalVersionManager(); + pmList.add(vm.getPersistenceManager()); + + String[] wspNames = _repositoryContext.getWorkspaceManager().getWorkspaceNames(); + for (int i = 0; i < wspNames.length; i++) + { + pmList.add(getPM(wspNames[i])); + } + + _pmList = new IterablePersistenceManager[pmList.size()]; + for (int i = 0; i < pmList.size(); i++) + { + PersistenceManager pm = pmList.get(i); + if (!(pm instanceof IterablePersistenceManager)) + { + _pmList = null; + break; + } + _pmList[i] = (IterablePersistenceManager) pm; + } + + // Initialize the task progress object. + int count = 0; // number of item that will be scanned. + + try + { + for (IterablePersistenceManager pm : _pmList) + { + count += pm.getAllNodeIds(null, 0).size(); + } + + // When scan is finished, progress is set at 70%. + float total = (10f / 7) * count; + + _progress = new TaskProgress(Math.max(total, 1)); + } + catch (Exception e) + { + _progress = new TaskProgress(0); + _progress.setInErrorState(e); + _report.addException(e); + } + } + + @Override + protected void apply() throws RepositoryException + { + // call System.gc() a few times before running the data store garbage collection. + // Please note System.gc() does not guarantee all objects are garbage collected. + for (int i = 0; i < SYSTEM_GC_CALLS; i++) + { + System.gc(); + } + + // Log some informations about the ds. + int startSize = reportDataStoreInfo(_garbageCollector.getDataStore()); + + // Sleep + if (_garbageCollector.getDataStore() instanceof FileDataStore) + { + // make sure the file is old (access time resolution is 2 seconds) + try + { + Thread.sleep(2000); + } + catch (InterruptedException e) + { + _report.addException(e); + throw new RuntimeException(e); + } + } + + // GC Management + _garbageCollector.setMarkEventListener(this); + _garbageCollector.setPersistenceManagerScan(true); + // gc.setSleepBetweenNodes(0); + + // Run GC + try + { + _scannedNodesCount = 0; + _report.add("Scanning the repository nodes..."); + _garbageCollector.mark(); + _report.add(_scannedNodesCount + " nodes scanned."); + + // Only for tests purpose (decomment) + // _garbageCollector.getDataStore().clearInUse(); + + _report.add("Deleting unused items... Please be patient."); + int deleted = _garbageCollector.sweep(); + _report.add(deleted + " unused items deleted."); + if (_progress != null) + { + _progress.progressRelativePercentage(50); + } + + _report.add("Finalizing the process..."); + + // Compressing derby DATASTORE table. + // A Workaround is used to detect that we are using derby for the datastore + // because we are in fact using the DbDataStore. In reality, we should simply use _garbageCollector.getDataStore() instanceof DerbyDataStore + // See REPOSITORY-209 + if (_garbageCollector.getDataStore() instanceof DbDataStore && "derby".equals(((DbDataStore) _garbageCollector.getDataStore()).getDatabaseType())) + { + _report.add("Reclaiming unused space, this may take several minutes depending on the size of the data store."); + _report.add("Please be patient."); + + DbDataStore ds = (DbDataStore) _garbageCollector.getDataStore(); + derbyCompressTable(ds); + // TODO try SYSCS_DIAG.SPACE_TABLE to estimate the amount of unused space in a table or index + // derby 10.6+ only ? + } + } + finally + { + _garbageCollector.close(); + } + + // Log some informations about the ds. + int finalSize = reportDataStoreInfo(_garbageCollector.getDataStore()); + int freedSize = startSize - finalSize; + _report.add("Size of cleared data : " + Math.round(freedSize / 1024f) + "Ko"); + _report.add("The total released space on your disk can be different depending on the type of the data store used by your repository."); + } + + @Override + protected void close() + { + if (_session != null) + { + _session.logout(); + } + + if (_repositoryContext != null && _repositoryContext.getRepository() != null) + { + _repositoryContext.getRepository().shutdown(); + } + + if (_progress != null) + { + _progress.progressRelativePercentage(100); + } + } + + private int reportDataStoreInfo(DataStore ds) throws DataStoreException + { + int count = 0; + int total = 0; + Iterator it = ds.getAllIdentifiers(); + while (it.hasNext()) + { + count++; + DataIdentifier id = it.next(); + total += ds.getRecord(id).getLength(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("Datastore item count : ").append(count).append(" "); + sb.append("[total size : ").append(Math.round(total / 1024f)).append("Ko]"); + _report.add(sb.toString()); + + return total; + } + + /** + * Reclaiming unused space. This is derby specific. + * By default, Derby does not return unused space to the operating system + * when updating or deleting data. + * @param ds + * @throws RepositoryException + */ + private void derbyCompressTable(DbDataStore ds) throws RepositoryException + { + DataSource dataSource = null; + + try + { + ConnectionFactory cf = _repositoryConfig.getConnectionFactory(); + if (ds.getDataSourceName() == null || "".equals(ds.getDataSourceName())) + { + dataSource = cf.getDataSource(ds.getDriver(), ds.getUrl(), ds.getUser(), ds.getPassword()); + } + else + { + dataSource = cf.getDataSource(ds.getDataSourceName()); + } + } + catch (SQLException e) + { + _report.addException(e); + throw new RuntimeException(e); + } + + if (dataSource != null) + { + ConnectionHelper conHelper = new ConnectionHelper(dataSource, false); +// String sql = "CALL SYSCS_UTIL.SYSCS_COMPRESS_TABLE(CURRENT SCHEMA, ?, 1)"; + String sql = "CALL SYSCS_UTIL.SYSCS_INPLACE_COMPRESS_TABLE(CURRENT SCHEMA, ?, 1, 1, 1)"; + String prefix = ds.getTablePrefix(); + String schemaObjPrefix = ds.getSchemaObjectPrefix(); + String table = prefix + schemaObjPrefix + "DATASTORE"; + + try + { + conHelper.query(sql, table); + } + catch (SQLException e) + { + _report.addException(e); + throw new RuntimeException(e); + } + } + else + { + _report.add(TaskReportMessageType.ERROR, "Unable to compress the Derby datastore, unused space has not been freed up."); + } + + } + + @Override + public void beforeScanning(Node n) throws RepositoryException + { + _scannedNodesCount++; + if (_progress != null) + { + _progress.progress(); + } + } + + /** + * Retrieves JackRabbit Persistence Manager for currently opened repository. This method uses + * Privileged access and will fail with security exception if used in environment with enabled security manager. + * + * @return Persistence manager used by repository. + * @throws RepositoryException + */ + private PersistenceManager getPM(String workspaceName) throws RepositoryException + { + try + { + Object workspaceInfo = findAndInvokeMethod(_repositoryContext.getRepository(), "getWorkspaceInfo", new Object[] {workspaceName}); + return (PersistenceManager) (findAndInvokeMethod(workspaceInfo, "getPersistenceManager", null)); + } + catch (Exception e) + { + throw new RepositoryException(e); + } + } + + private static Object findAndInvokeMethod(Object obj, String name, Object[] parameters) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + Method m = null; + Method[] ms = obj.getClass().getDeclaredMethods(); + for (int i = 0; i < ms.length; i++) + { + final Method x = ms[i]; + if (x.getName().equals(name)) + { + m = x; + break; + } + } + m.setAccessible(true); + return m.invoke(obj, parameters); + } +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceTaskInfoGenerator.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceTaskInfoGenerator.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceTaskInfoGenerator.java (revision 0) @@ -0,0 +1,117 @@ +/* + * Copyright 2010 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.workspaces.repository; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.generation.ServiceableGenerator; +import org.apache.cocoon.xml.AttributesImpl; +import org.apache.cocoon.xml.XMLUtils; +import org.xml.sax.SAXException; + +import org.ametys.workspaces.repository.tasks.MaintenanceTaskManager; +import org.ametys.workspaces.repository.tasks.core.TaskReport.TaskReportMessage; + +/** + * Generate informations about the running maintenance task progress. + */ +public class MaintenanceTaskInfoGenerator extends ServiceableGenerator +{ + /** The maintenance task manager */ + private MaintenanceTaskManager _taskManager; + + @Override + public void service(ServiceManager m) throws ServiceException + { + super.service(m); + _taskManager = (MaintenanceTaskManager) m.lookup(MaintenanceTaskManager.ROLE); + } + + public void generate() throws IOException, SAXException, ProcessingException + { + Map jsParameters = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); + + contentHandler.startDocument(); + + // Is task finished ? + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("isRunning", Boolean.toString(_taskManager.isTaskRunning())); + XMLUtils.startElement(contentHandler, "task", attrs); + + // Sax report + int part = (Integer) jsParameters.get("part"); + List report = new ArrayList(); + Integer lastPart = _taskManager.getReportMessage(report, part); + if (lastPart != null) + { + _saxReport(report, lastPart); + } + + // Sax progress + _saxProgress(_taskManager.getProgressInfo()); + + // Sax repository state + _saxRepositoryState(); + + XMLUtils.endElement(contentHandler, "task"); + contentHandler.endDocument(); + } + + private void _saxRepositoryState() throws SAXException + { + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("state", Boolean.toString(_taskManager.isRepositoryStarted())); + XMLUtils.createElement(contentHandler, "repository", attrs); + } + + private void _saxProgress(Map progress) throws SAXException + { + if (progress != null && !progress.isEmpty()) + { + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("progress", progress.get("progress")); + attrs.addCDATAAttribute("total", progress.get("total")); + attrs.addCDATAAttribute("percentage", progress.get("percentage")); + attrs.addCDATAAttribute("state", progress.get("state")); + + XMLUtils.createElement(contentHandler, "progress", attrs); + } + } + + private void _saxReport(List report, int lastPart) throws SAXException + { + AttributesImpl attrs = new AttributesImpl(); + attrs.addCDATAAttribute("lastPart", String.valueOf(lastPart)); + XMLUtils.startElement(contentHandler, "report", attrs); + + for (TaskReportMessage reportMessage : report) + { + AttributesImpl attrs_msg = new AttributesImpl(); + attrs_msg.addCDATAAttribute("type", reportMessage.getTaskReportMessageType().name().toLowerCase()); + XMLUtils.createElement(contentHandler, "entry", attrs_msg, reportMessage.getMessage()); + } + + XMLUtils.endElement(contentHandler, "report"); + } + +} Index: main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceLaunchTaskAction.java =================================================================== --- main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceLaunchTaskAction.java (revision 0) +++ main/workspace-repository/src/org/ametys/workspaces/repository/MaintenanceLaunchTaskAction.java (revision 0) @@ -0,0 +1,71 @@ +/* + * Copyright 2010 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.workspaces.repository; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.avalon.framework.thread.ThreadSafe; +import org.apache.cocoon.acting.ServiceableAction; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Redirector; +import org.apache.cocoon.environment.SourceResolver; + +import org.ametys.workspaces.repository.tasks.MaintenanceTaskManager; +import org.ametys.workspaces.repository.tasks.MaintenanceTaskManager.MaintenanceTaskType; + +/** + * Action that launch a maintenance task. + */ +public class MaintenanceLaunchTaskAction extends ServiceableAction implements ThreadSafe +{ + /** The maintenance task manager */ + private MaintenanceTaskManager _taskManager; + + @Override + public void service(ServiceManager m) throws ServiceException + { + super.service(m); + _taskManager = (MaintenanceTaskManager) m.lookup(MaintenanceTaskManager.ROLE); + } + + public Map act(Redirector redirector, SourceResolver sourceResolver, Map objectModel, String source, Parameters parameters) throws Exception + { + Map result = new HashMap(); + Map jsParameters = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); + + String task = (String) jsParameters.get("task"); + + // Launch the task + try + { + boolean launched = _taskManager.launch(MaintenanceTaskType.valueOf(task)); + if (launched) + { + result.put("launched", "true"); + } + } + catch (Exception e) + { + getLogger().error(e.getMessage(), e); + } + + return result; + } +} Index: main/workspace-repository/sitemap.xmap =================================================================== --- main/workspace-repository/sitemap.xmap (revision 19543) +++ main/workspace-repository/sitemap.xmap (working copy) @@ -36,6 +36,9 @@ + + + @@ -54,8 +57,13 @@ + + + + + - + @@ -375,6 +383,26 @@ + + + + + + + + + + + + + + + + + + + + Index: main/workspace-repository/resources/css/home.css =================================================================== --- main/workspace-repository/resources/css/home.css (revision 19543) +++ main/workspace-repository/resources/css/home.css (working copy) @@ -391,3 +391,77 @@ font-weight: normal; } +/* @end Admin */ + +/** @ Maintenance panel */ +.maintenance-intro-title { + color:#15428B; + font-family:tahoma,arial,verdana,sans-serif !important; + font-size:11px !important; + font-weight:bold !important; +} + +.maintenance-hint-panel { + background-color:#E8F2FF; + border:1px solid #A9BFD3 !important; + color:#404040; + font-size:0.9em; + font-style:italic; + padding:5px; +} + +.maintenance-hint-panel .x-panel-body { + background-color: #e8f2ff !important; + background-image: url('../img/help.png'); + background-repeat: no-repeat; + padding-left: 20px; +} + +#maintenance-progress-panel { +} + +#maintenance-report-panel { + padding-top: 1px; + padding-left: 5px; + font-size:0.9em; + font-style: italic; +} +#maintenance-report-panel .x-panel { + margin: 2px; +} + +#maintenance-report-panel .report-info { + color: #404040; +} +#maintenance-report-panel .report-warn { + color: #ee4000; +} +#maintenance-report-panel .report-error { + font-weight: bold; + font-style: normal; + color: #ee4000; +} + +#maintenance-tab .garbage-collector_btn { + background-image: url('../img/maintenance/garbage_collector_64.png') !important; + width:64px !important; + height:64px !important; + margin-right: auto !important; + margin-left: auto !important; +} +#maintenance-tab .reindexing_btn { + background-image: url('../img/maintenance/reindexing_64.png') !important; + width:64px !important; + height:64px !important; + margin-right: auto !important; + margin-left: auto !important; +} +#maintenance-tab .consistency-check_btn { + background-image: url('../img/maintenance/consistency_check_64.png') !important; + width:64px !important; + height:64px !important; + margin-right: auto !important; + margin-left: auto !important; +} + +/* @end Maintenance */ Index: main/workspace-repository/resources/js/org/ametys/repository/HomePage.i18n.js =================================================================== --- main/workspace-repository/resources/js/org/ametys/repository/HomePage.i18n.js (revision 19543) +++ main/workspace-repository/resources/js/org/ametys/repository/HomePage.i18n.js (working copy) @@ -36,6 +36,7 @@ } org.ametys.repository.HomePage._tabs.push(new org.ametys.repository.AdminTab()); + org.ametys.repository.HomePage._tabs.push(new org.ametys.repository.MaintenanceTab()); org.ametys.repository.HomePage._tabs.push(new org.ametys.repository.ConsoleTab()); } Index: main/workspace-repository/resources/js/org/ametys/repository/MaintenanceTab.i18n.js =================================================================== --- main/workspace-repository/resources/js/org/ametys/repository/MaintenanceTab.i18n.js (revision 0) +++ main/workspace-repository/resources/js/org/ametys/repository/MaintenanceTab.i18n.js (revision 0) @@ -0,0 +1,476 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.namespace('org.ametys.repository'); + +/** + * Create the panel + */ +org.ametys.repository.MaintenanceTab = function () +{ + var me = this; + + // The task that will be handled by the Ext.TaskMgr + this._taskInfoTask = { + run: me._getTaskInfo, + scope: me, + interval: 1000 + }; + + // Contains this task ID of the running task, null if there is no task running. + this._runningTask = null; + // True if the last server response was a bad response. + this._badServerResponse = false; + // Indicates the last report part that has been displayed to the client (~ report cursor) + this._currentReportPart = 0; + + // 1 - Tool bar = title + repository state + this._repositoryStateBtn = new Ext.Button({ + icon: getWorkspaceResources() + '/img/maintenance/repository_on_16.png', + tooltip: "Repo state tootltip" + }); + var tbar = [ + new Ext.form.Label({ + text: "Taches de maintenance", + cls: "maintenance-intro-title" + }), + '->', + this._repositoryStateBtn + ]; + + // 2 -Hint panel + this._hintPanel = new Ext.Panel ({ + border: false, + cls: 'maintenance-hint-panel', + html: "" + }); + + // 3 - Tasks buttons panel (menu) + var taskBtns = []; + for (var task in this.Tasks) + { + taskBtns.push({ + xtype:'button', + tooltip: this.Tasks[task].text, + iconCls: this.Tasks[task].buttonCls, + disabled: true, + scope: me, + handler: me._launchTask, + itemId: task + }); + } + + this._taskButtonsPanel = new Ext.Toolbar ({ + layout: 'hbox', + layoutConfig: { + align: 'stretch' + }, + height: 75, + + items: [ + { + xtype : 'spacer', + flex: 0.5 + }, + taskBtns[0], + { + xtype : 'spacer', + flex: 1 + }, + taskBtns[1], + { + xtype : 'spacer', + flex: 1 + }, + taskBtns[2], + { + xtype : 'spacer', + flex: 0.5 + } + ] + }); + + // 4 - Progress bar + this._progressBar = new Ext.ProgressBar(); + this._progressPanel = new Ext.Panel ({ + layout: 'fit', + border: false, + id: 'maintenance-progress-panel', + items: this._progressBar + }); + this._progressPanel.hide(); + + // 5 - Report panel + this._reportPanel = new Ext.Panel ({ + id: 'maintenance-report-panel', + border : false, + anchor : '-20' + }); + var reportPanelContainer = new Ext.Panel ({ + layout: 'anchor', + border: false, + flex: 1, + autoScroll: true, + items : [ + this._reportPanel + ] + }); + + // Task panel = progress + report + this._taskPanel = new Ext.Panel ({ + border: false, + layout: 'vbox', + layoutConfig: { + align: 'stretch' + }, + flex: 1, + items: [ + this._progressPanel, + reportPanelContainer + ] + }); + + // Returns the global panel : Toolbar + hint panel + task buttons panel + task panel + return new Ext.Panel ({ + title: "", + id: 'maintenance-tab', + region: 'center', + border: true, + layout: 'vbox', + layoutConfig: { + align: 'stretch' + }, + + tbar : tbar, + + items: [ + this._hintPanel, + this._taskButtonsPanel, + this._taskPanel + ], + + listeners: { + 'beforeshow': { + fn: me._onBeforeShow, + scope: me + }, + 'beforehide' : { + fn: me._onBeforeHide, + scope: me + } + } + }); +}; + +/** + * Enumeration of the available tasks + */ +org.ametys.repository.MaintenanceTab.prototype.Tasks = { + DATA_STORE_GARBAGE_COLLECTOR : { + text: "", + buttonCls: "garbage-collector_btn" + }, + REINDEXING : { + text: "", + buttonCls: "reindexing_btn" + }, + CONSISTENCY_CHECK : { + text: "", + buttonCls: "consistency-check_btn" + } +} + +org.ametys.repository.MaintenanceTab.prototype._onBeforeShow = function() +{ + // Disable task btns + this._disableTaskBtns(true); + + // Launch task request. + var serverMessage = new org.ametys.servercomm.ServerMessage( + '_' + context.workspaceName, + '/repository/maintenance/is-running-task', + null, + org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, + this._onBeforeShowCb, + this, + null); + org.ametys.servercomm.ServerComm.getInstance().send(serverMessage); +} + +org.ametys.repository.MaintenanceTab.prototype._onBeforeShowCb = function(response, args) +{ + if (org.ametys.servercomm.ServerComm.handleBadResponse("", response, "org.ametys.repository.MaintenanceTab._onBeforeShow")) + { + this._addReportErrorLog(""); + return; + } + + // Update repository state + var task = response.selectSingleNode("task"); + this._updateRepositoryState(task.getAttribute("repository-state") === "true"); + + // Ajax requests are launched if a task is currently running; + // or if a task has finished but a part of the report was already displayed on the screen, + // the end of the report must be displayed. + if (task.getAttribute("running") === "true" || this._currentReportPart > 0) + { + if (task.getAttribute("running") === "true") + { + this._runningTask = task[org.ametys.servercomm.ServerComm.xmlTextContent]; + this._taskButtonsPanel.getComponent(this._runningTask).toggle(true); + } + + if (!this._progressPanel.isVisible()) + { + this._progressPanel.show(); + this._taskPanel.doLayout(); + } + + if (this._currentReportPart === 0) + { + this._reportPanel.removeAll(); + } + + // Ajax request : get task progress info every {interval} ms. + this._stopGetTaskInfo = false; + Ext.TaskMgr.start(this._taskInfoTask); + } + else + { + this._disableTaskBtns(false); + this._runningTask = null; + this._currentReportPart = 0; + } +} + +org.ametys.repository.MaintenanceTab.prototype._onBeforeHide = function() +{ + // This will stop the Ext.TaskMgr + // Workaround because the Ext.TaskMgr.stop(this._taskInfoTask); was not working as excepted. + this._stopGetTaskInfo = true; +} + +org.ametys.repository.MaintenanceTab.prototype._disableTaskBtns = function (disable) +{ + for (var task in this.Tasks) + { + var taskBtn = this._taskButtonsPanel.getComponent(task); + if (disable) + { + taskBtn.disable(); + } + else + { + taskBtn.enable(); + taskBtn.toggle(false); + } + } +} + +org.ametys.repository.MaintenanceTab.prototype._launchTask = function (btn, e) +{ + if (this._runningTask) + { + return; + } + + btn.toggle(true); + + this._runningTask = btn.itemId; // itemId of the btn contains the taskId. + this._disableTaskBtns(true); + this._reportPanel.removeAll(); + + if (!this._progressPanel.isVisible()) + { + this._progressPanel.show(); + this._taskPanel.doLayout(); + } + + // Launch task request. + var serverMessage = new org.ametys.servercomm.ServerMessage( + '_' + context.workspaceName, + '/repository/maintenance/launch-task', + {task : this._runningTask}, + org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, + this._launchTaskCb, + this, + null); + org.ametys.servercomm.ServerComm.getInstance().send(serverMessage); +}; + +org.ametys.repository.MaintenanceTab.prototype._launchTaskCb = function (response, args) +{ + // Handle error + if (org.ametys.servercomm.ServerComm.handleBadResponse("", response, "org.ametys.repository.MaintenanceTab._launchTask")) + { + this._addReportErrorLog(""); + return; + } + + // Update repository state + var task = response.selectSingleNode("task"); + this._updateRepositoryState(task.getAttribute("repository-state") === "true"); + + var isLaunched = task.getAttribute("launched") === "true"; + if (!isLaunched) + { + this._onTaskFinished(); + this._addReportErrorLog(""); + this._addReportLog(""); + return; + } + + // Ajax request : get task progress info every {interval} ms. + this._stopGetTaskInfo = false; + Ext.TaskMgr.start(this._taskInfoTask); +} + +org.ametys.repository.MaintenanceTab.prototype._getTaskInfo = function(count) +{ + if (this._stopGetTaskInfo === true) + { + return false; // Ext.TaskMgr will stop the running task. + } + + // Get task progress info. + var serverMessage = new org.ametys.servercomm.ServerMessage( + '_' + context.workspaceName, + '/repository/maintenance/get-task-info', + {part : this._currentReportPart}, + org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, + this._getTaskInfoCb, + this, + null); + org.ametys.servercomm.ServerComm.getInstance().send(serverMessage); + + return true; // Ext.TaskMgr will continue to run the task while this._stopGetTaskInfo is not true. +} + +org.ametys.repository.MaintenanceTab.prototype._onTaskFinished = function () +{ + this._disableTaskBtns(false); + this._runningTask = null; + this._badServerResponse = false; + this._currentReportPart = 0; +} + +org.ametys.repository.MaintenanceTab.prototype._getTaskInfoCb = function(response, args) +{ + // Prevents client from being spammed with error dialogs / error logs + if (this._badServerResponse && (response == null || response.getAttribute("code") == "500" || response.getAttribute("code") == "404")) + { + return; + } + + // Handle error + if (org.ametys.servercomm.ServerComm.handleBadResponse("", response, "org.ametys.repository.MaintenanceTab._getTaskInfo")) + { + this._addReportWarningLog(""); + this._badServerResponse = true; + return; + } + + this._badServerResponse = false; + var task = response.selectSingleNode("task"); + + // Update progress bar + var progress = task.selectSingleNode("progress"); + this._updateProgressBar(progress); + + // Update report panel + var report = task.selectSingleNode("report"); + this._addReportLogsFromXML(report); + + // Update repository state + var repository = task.selectSingleNode("repository"); + this._updateRepositoryState(repository.getAttribute("state") === "true"); + + // End task if needed. + if (task.getAttribute("isRunning") !== "true") + { + this._onTaskFinished(); + this._stopGetTaskInfo = true; + // Ext.TaskMgr.stop(this._taskInfoTask); + } +} + +org.ametys.repository.MaintenanceTab.prototype._updateProgressBar = function(progress) +{ + if (!progress) return; + + var current = progress.getAttribute("progress"); + var total = progress.getAttribute("total"); + var percentage = progress.getAttribute("percentage"); + var progressLabel = "" + Math.round(100 * percentage) + ""; + this._progressBar.updateProgress(percentage, progressLabel, false); +} + +org.ametys.repository.MaintenanceTab.prototype._addReportLogsFromXML = function(report) +{ + // Update this._currentReportPart + var lastPart = Number(report.getAttribute("lastPart")); + this._currentReportPart = !!lastPart ? lastPart : this._currentReportPart; + + // Add logs to the report panel. + var logs = report.selectNodes("entry"); + if(logs) + { + var log; + for (var i=0; i < logs.length; i++) + { + log = logs[i]; + this._addReportLog(log[org.ametys.servercomm.ServerComm.xmlTextContent], log.getAttribute("type"), true); + } + this._reportPanel.doLayout(); + } +} + +org.ametys.repository.MaintenanceTab.prototype._addReportLog = function(log, type, skipDoLayout) +{ + this._reportPanel.add({ + border: false, + html: log, + cls: 'report-' + (type || "info") + }); + + if (skipDoLayout !== true) + { + this._reportPanel.doLayout(); + } +} + +org.ametys.repository.MaintenanceTab.prototype._addReportWarningLog = function(log) +{ + this._addReportLog(log, "warn"); +} +org.ametys.repository.MaintenanceTab.prototype._addReportErrorLog = function(log) +{ + this._addReportLog(log, "error"); +} + +org.ametys.repository.MaintenanceTab.prototype._updateRepositoryState = function(on) +{ + var state = on ? 'on' : 'off'; + + if (this._repositoryState !== state) + { + this._repositoryState = state; + var iconPath = getWorkspaceResources() + '/img/maintenance/repository_' + state + '_16.png' + this._repositoryStateBtn.setIcon(iconPath); + } + +} + Index: main/workspace-repository/resources/js/org/ametys/repository/Actions.i18n.js =================================================================== --- main/workspace-repository/resources/js/org/ametys/repository/Actions.i18n.js (revision 19543) +++ main/workspace-repository/resources/js/org/ametys/repository/Actions.i18n.js (working copy) @@ -1281,9 +1281,10 @@ mainPanel.remove(mainPanel.items.get(0)); mainPanel.remove(mainPanel.items.get(0)); mainPanel.remove(mainPanel.items.get(0)); + mainPanel.remove(mainPanel.items.get(0)); mainPanel.doLayout(); - mainPanel.add([new org.ametys.repository.JCRViewTab(true), new org.ametys.repository.AdminTab(), new org.ametys.repository.ConsoleTab()]); + mainPanel.add([new org.ametys.repository.JCRViewTab(true), new org.ametys.repository.AdminTab(), new org.ametys.repository.MaintenanceTab(), new org.ametys.repository.ConsoleTab()]); mainPanel.setActiveTab(0); } \ No newline at end of file Index: main/plugin-repositoryapp/plugin.xml =================================================================== --- main/plugin-repositoryapp/plugin.xml (revision 19543) +++ main/plugin-repositoryapp/plugin.xml (working copy) @@ -93,4 +93,12 @@ + + + + + +