diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml index a99fbf944a..9815a87549 100644 --- a/config/alfresco/scheduled-jobs-context.xml +++ b/config/alfresco/scheduled-jobs-context.xml @@ -483,4 +483,17 @@ + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionImpl.java b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionImpl.java index 99e5ad6d0b..690151fecd 100644 --- a/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionImpl.java +++ b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionImpl.java @@ -22,6 +22,9 @@ import java.util.Date; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedAction; +import org.alfresco.service.cmr.repository.NodeRef; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; /** * The scheduling wrapper around a persisted @@ -33,17 +36,26 @@ import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedAction; */ public class ScheduledPersistedActionImpl implements ScheduledPersistedAction { + private NodeRef persistedAtNodeRef; private Action action; private Date scheduleStart; private Integer intervalCount; private IntervalPeriod intervalPeriod; - public ScheduledPersistedActionImpl(Action action) + protected ScheduledPersistedActionImpl(Action action) { this.action = action; } + /** + * Get the persisted nodeRef for this schedule + */ + protected NodeRef getPersistedAtNodeRef() + { + return persistedAtNodeRef; + } + /** Get the action which the schedule applies to */ public Action getAction() { @@ -123,4 +135,72 @@ public class ScheduledPersistedActionImpl implements ScheduledPersistedAction } return intervalCount.toString() + intervalPeriod.getLetter(); } + + /** + * Returns a Quartz trigger definition based on the current + * scheduling details. + * May only be called once this object has been persisted + */ + public Trigger asTrigger() + { + if(persistedAtNodeRef == null) + throw new IllegalStateException("Must be persisted first"); + + // Use our nodeRef as the unique title + String triggerName = persistedAtNodeRef.toString(); + + // Monthly is a special case, since the period + // will vary + // TODO - Make more things use DateIntervalTrigger + if(intervalPeriod == IntervalPeriod.Month) + { +// TODO +// DateIntervalTrigger trigger = new DateIntervalTrigger( +// triggerName, null, +// scheduleStart, null, +// DateIntervalTrigger.IntervalUnit.MONTH, +// intervalCount +// ); +// trigger.setMisfireInstruction( DateIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_NOW ); + } + + SimpleTrigger trigger = null; + + // Is it Start Date + Repeat Interval? + if(scheduleStart != null && getScheduleInterval() != null) + { + trigger = new SimpleTrigger( + triggerName, null, + scheduleStart, null, + SimpleTrigger.REPEAT_INDEFINITELY, + intervalCount * intervalPeriod.getInterval() + ); + } + + // Is it a single Start Date? + if(scheduleStart != null && getScheduleInterval() == null) + { + trigger = new SimpleTrigger( + triggerName, null, + scheduleStart + ); + } + + // Is it start now, run with Repeat Interval? + if(getScheduleInterval() != null) + { + trigger = new SimpleTrigger( + triggerName, null, + SimpleTrigger.REPEAT_INDEFINITELY, + intervalCount * intervalPeriod.getInterval() + ); + } + + if(trigger != null) + { + // If we miss running, run as soon after as we can + trigger.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW); + } + return trigger; + } } diff --git a/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceImpl.java b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceImpl.java index 0c97b45f32..e4d00e8331 100644 --- a/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceImpl.java +++ b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceImpl.java @@ -18,11 +18,31 @@ */ package org.alfresco.repo.action.scheduled; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.alfresco.repo.action.ActionModel; +import org.alfresco.repo.action.RuntimeActionService; import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedAction; import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedActionService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; /** * A service which handles the scheduling of the @@ -34,33 +54,204 @@ import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedActionService * @author Nick Burch * @since 3.4 */ -public interface ScheduledPersistedActionServiceImpl extends ScheduledPersistedActionService { +public class ScheduledPersistedActionServiceImpl implements ScheduledPersistedActionService { + protected static final String SCHEDULED_ACTION_ROOT_PATH = + "/app:company_home/app:dictionary/cm:Scheduled_x0020_Actions"; + protected static NodeRef SCHEDULED_ACTION_ROOT_NODE_REF; + protected static final Set ACTION_TYPES = new HashSet( + Arrays.asList(new QName[] { ActionModel.TYPE_ACTION })); // TODO + + protected static final String SCHEDULER_GROUP = "PersistedActions"; + + private static final Log log = LogFactory.getLog(ScheduledPersistedActionServiceImpl.class); + + private Scheduler scheduler; + private NodeService nodeService; + private NodeService startupNodeService; + private ActionService actionService; + private SearchService startupSearchService; + private NamespaceService namespaceService; + private RuntimeActionService runtimeActionService; + + public void setScheduler(Scheduler scheduler) + { + this.scheduler = scheduler; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the node service to use during startup, which + * won't do permissions check etc + */ + public void setStartupNodeService(NodeService startupNodeService) + { + this.startupNodeService = startupNodeService; + } + + public void setStartupSearchService(SearchService startupSearchService) + { + this.startupSearchService = startupSearchService; + } + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + public void setRuntimeActionService(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + + /** + * Find all our previously persisted scheduled actions, and + * tell the scheduler to start handling them. + * Called by spring when startup is complete. + */ + public void schedulePreviouslyPersisted() { + // Grab the path of our bit of the data dictionary + StoreRef spacesStore = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + List nodes = startupSearchService.selectNodes( + startupNodeService.getRootNode(spacesStore), + SCHEDULED_ACTION_ROOT_PATH, + null, namespaceService, false + ); + if(nodes.size() != 1) + { + throw new IllegalStateException("Tries to find the Scheduled Actions Data Dictionary" + + " folder at " + SCHEDULED_ACTION_ROOT_PATH + " but got " + nodes.size() + "results"); + } + SCHEDULED_ACTION_ROOT_NODE_REF = nodes.get(0); + + // Now, look up our persisted actions and schedule + List actions = listSchedules(startupNodeService); + for(ScheduledPersistedAction action : actions) + { + addToScheduler((ScheduledPersistedActionImpl)action); + } + } + + /** * Creates a new schedule, for the specified Action. */ - public ScheduledPersistedAction createSchedule(Action persistedAction); + public ScheduledPersistedAction createSchedule(Action persistedAction) + { + return new ScheduledPersistedActionImpl(persistedAction); + } /** * Saves the changes to the schedule to the repository, * and updates the Scheduler with any changed details. */ - public void saveSchedule(ScheduledPersistedAction schedule); + public void saveSchedule(ScheduledPersistedAction schedule) + { + removeFromScheduler((ScheduledPersistedActionImpl)schedule); + addToScheduler((ScheduledPersistedActionImpl)schedule); + + // TODO + } /** * Removes the schedule for the action, and cancels future * executions of it. * The persisted action is unchanged. */ - public void deleteSchedule(ScheduledPersistedAction schedule); + public void deleteSchedule(ScheduledPersistedAction schedule) + { + removeFromScheduler((ScheduledPersistedActionImpl)schedule); + + // TODO + } /** * Returns the schedule for the specified action, or * null if it isn't currently scheduled. */ - public ScheduledPersistedAction getSchedule(Action persistedAction); + public ScheduledPersistedAction getSchedule(Action persistedAction) + { + // TODO + return null; + } /** * Returns all currently scheduled actions. */ - public List listSchedules(); + public List listSchedules() + { + return listSchedules(nodeService); + } + private List listSchedules(NodeService nodeService) + { + List childAssocs = nodeService.getChildAssocs(SCHEDULED_ACTION_ROOT_NODE_REF, ACTION_TYPES); + + List scheduledActions = new ArrayList(childAssocs.size()); + for (ChildAssociationRef actionAssoc : childAssocs) + { + // TODO +// Action nextAction = runtimeActionService.createAction(actionAssoc.getChildRef()); +// renderingActions.add(new ReplicationDefinitionImpl(nextAction)); + } + + return scheduledActions; + } + + + /** + * Takes an entry out of the scheduler, if it's currently + * there. + */ + protected void removeFromScheduler(ScheduledPersistedActionImpl schedule) + { + // Jobs are indexed by the persisted node ref + try { + scheduler.deleteJob( + schedule.getPersistedAtNodeRef().toString(), + SCHEDULER_GROUP + ); + } catch (SchedulerException e) { + // Probably means scheduler is shutting down + log.warn(e); + } + } + /** + * Adds a new entry into the scheduler + */ + protected void addToScheduler(ScheduledPersistedActionImpl schedule) + { + // Wrap it up in Quartz bits + JobDetail details = buildJobDetail(schedule); + Trigger trigger = schedule.asTrigger(); + + // Schedule it + try { + scheduler.scheduleJob(details, trigger); + } catch (SchedulerException e) { + // Probably means scheduler is shutting down + log.warn(e); + } + } + + protected JobDetail buildJobDetail(ScheduledPersistedActionImpl schedule) + { + JobDetail detail = new JobDetail( + schedule.getPersistedAtNodeRef().toString(), + SCHEDULER_GROUP, + null // TODO + ); + + // TODO + + return detail; + } } diff --git a/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceTest.java b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceTest.java new file mode 100644 index 0000000000..622d0cdfba --- /dev/null +++ b/source/java/org/alfresco/repo/action/scheduled/ScheduledPersistedActionServiceTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.action.scheduled; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedAction; +import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedActionService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Unit tests for the {@link ScheduledPersistedActionService} + */ +public class ScheduledPersistedActionServiceTest extends TestCase +{ + private static ConfigurableApplicationContext ctx = + (ConfigurableApplicationContext)ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private ActionService actionService; + private NodeService nodeService; + private Repository repositoryHelper; + + private NodeRef scheduledRoot; + + @Override + protected void setUp() throws Exception + { + actionService = (ActionService) ctx.getBean("actionService"); + nodeService = (NodeService) ctx.getBean("nodeService"); + repositoryHelper = (Repository) ctx.getBean("repositoryHelper"); + transactionService = (TransactionService) ctx.getBean("transactionService"); + + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + UserTransaction txn = transactionService.getUserTransaction(); + txn.begin(); + + // Zap any existing persisted entries + scheduledRoot = ScheduledPersistedActionServiceImpl.SCHEDULED_ACTION_ROOT_NODE_REF; + for(ChildAssociationRef child : nodeService.getChildAssocs(scheduledRoot)) + { + QName type = nodeService.getType( child.getChildRef() ); + if(ScheduledPersistedActionServiceImpl.ACTION_TYPES.contains(type)) + { + nodeService.deleteNode(child.getChildRef()); + } + } + + // Finish setup + txn.commit(); + } + + @Override + protected void tearDown() throws Exception { + UserTransaction txn = transactionService.getUserTransaction(); + txn.begin(); + + + txn.commit(); + } + + + /** + * Test that the {@link ScheduledPersistedAction} implementation + * behaves properly + */ + public void testPersistedActionImpl() throws Exception + { + // TODO + } + + /** + * Tests that the to-trigger stuff works properly + */ + + /** + * Tests that we can create, save, edit, delete etc the + * scheduled persisted actions + */ + + /** + * Tests that the listings work, both of all scheduled, + * and from an action + */ + + /** + * Tests that things actually get run correctly + */ + public void testExecution() throws Exception + { + // A job due to start in 2 seconds + // TODO + + // A job that runs every 2 seconds + // TODO + + // A job that starts in 2 seconds time, and runs + // every second + // TODO + } +} diff --git a/source/java/org/alfresco/repo/replication/ReplicationDefinitionPersisterImpl.java b/source/java/org/alfresco/repo/replication/ReplicationDefinitionPersisterImpl.java index 78310d1f2e..c4ebfb34ed 100644 --- a/source/java/org/alfresco/repo/replication/ReplicationDefinitionPersisterImpl.java +++ b/source/java/org/alfresco/repo/replication/ReplicationDefinitionPersisterImpl.java @@ -86,14 +86,14 @@ public class ReplicationDefinitionPersisterImpl implements ReplicationDefinition List childAssocs = nodeService.getChildAssocs(REPLICATION_ACTION_ROOT_NODE_REF, ACTION_TYPES); - List renderingActions = new ArrayList(childAssocs.size()); + List replicationActions = new ArrayList(childAssocs.size()); for (ChildAssociationRef actionAssoc : childAssocs) { Action nextAction = runtimeActionService.createAction(actionAssoc.getChildRef()); - renderingActions.add(new ReplicationDefinitionImpl(nextAction)); + replicationActions.add(new ReplicationDefinitionImpl(nextAction)); } - return renderingActions; + return replicationActions; } public List loadReplicationDefinitions(String targetName) diff --git a/source/java/org/alfresco/service/cmr/action/scheduled/ScheduledPersistedAction.java b/source/java/org/alfresco/service/cmr/action/scheduled/ScheduledPersistedAction.java index b1b7949aad..ac70e1753c 100644 --- a/source/java/org/alfresco/service/cmr/action/scheduled/ScheduledPersistedAction.java +++ b/source/java/org/alfresco/service/cmr/action/scheduled/ScheduledPersistedAction.java @@ -86,18 +86,28 @@ public interface ScheduledPersistedAction public static enum IntervalPeriod { - Month ('M'), - Week ('W'), - Day ('D'), - Hour ('h'), - Minute ('m'); + Month ('M', -1), + Week ('W', 7*24*60*60*1000), + Day ('D', 24*60*60*1000), + Hour ('h', 60*60*1000), + Minute ('m', 60*1000); private final char letter; - IntervalPeriod(char letter) { + private final long interval; + + IntervalPeriod(char letter, long interval) { this.letter = letter; + this.interval = interval; } public char getLetter() { return letter; } + /** + * Returns the interval of one of these + * periods, in milliseconds + */ + public long getInterval() { + return interval; + } } }