Implementing Signal activities and services for Orchard.Workflows

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2013-05-02 16:55:38 -07:00
parent bbe2bedf25
commit 655e2fa0b4
14 changed files with 321 additions and 51 deletions

View File

@@ -4,15 +4,20 @@ using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class WebResponseActivity : Event {
public WebResponseActivity() {
/// <summary>
/// Represents a named event which can be triggered by any kind of activity.
/// </summary>
public class SignalActivity : Event {
public const string SignalEventName = "Signal";
public SignalActivity() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override bool CanExecute(WorkflowContext workflowContext, ActivityContext activityContext) {
return true;
return activityContext.GetState<string>(SignalEventName) == workflowContext.Tokens[SignalEventName].ToString();
}
public override bool CanStartWorkflow {
@@ -28,15 +33,21 @@ namespace Orchard.Workflows.Activities {
}
public override string Name {
get { return "WebResponse"; }
get { return SignalEventName; }
}
public override LocalizedString Category {
get { return T("HTTP"); }
get { return T("Events"); }
}
public override LocalizedString Description {
get { return T("Suspends the workflow until an HTTP request comes in."); }
get { return T("Suspends the workflow until this signal is specifically triggered."); }
}
public override string Form {
get {
return "SignalEvent";
}
}
}
}

View File

@@ -120,8 +120,8 @@ namespace Orchard.Workflows.Activities {
foreach (var action in awaiting) {
var tokens = new Dictionary<string, object> { { "Content", _contentManager.Get(action.WorkflowRecord.ContentItemRecord.Id, VersionOptions.Latest) } };
var contentItem = _contentManager.Get(action.WorkflowRecord.ContentItemRecord.Id, VersionOptions.Latest);
var tokens = new Dictionary<string, object> { { "Content", contentItem } };
var workflowState = FormParametersHelper.FromJsonString(action.WorkflowRecord.State);
workflowState.TimerActivity_StartedUtc = null;
action.WorkflowRecord.State = FormParametersHelper.ToJsonString(workflowState);

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class TriggerActivity : Task {
private readonly IWorkflowManager _workflowManager;
public TriggerActivity(IWorkflowManager workflowManager) {
_workflowManager = workflowManager;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override bool CanExecute(WorkflowContext workflowContext, ActivityContext activityContext) {
return true;
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext) {
yield return T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext) {
var tokens = new Dictionary<string, object> { { "Content", workflowContext.Content }, { SignalActivity.SignalEventName, activityContext.GetState<string>(SignalActivity.SignalEventName) } };
_workflowManager.TriggerEvent(SignalActivity.SignalEventName, workflowContext.Content, () => tokens);
yield return T("Done");
}
public override string Name {
get { return "Trigger"; }
}
public override LocalizedString Category {
get { return T("Events"); }
}
public override LocalizedString Description {
get { return T("Triggers a Signal by its name."); }
}
public override string Form {
get {
return "Trigger";
}
}
}
}

View File

@@ -85,7 +85,7 @@ namespace Orchard.Workflows.Activities {
}
public override LocalizedString Description {
get { return T("Performs an HTTP GET or POST request on the specified URL and stores the response as part of the workflow instance."); }
get { return T("Performs an HTTP request."); }
}
public override string Form {

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.Logging;
using Orchard.Workflows.Activities;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Controllers {
public class SignalController : Controller {
private readonly IWorkflowManager _workflowManager;
private readonly ISignalService _genericEventService;
private readonly IContentManager _contentManager;
public SignalController(
IWorkflowManager workflowManager,
ISignalService genericEventService,
IContentManager contentManager) {
_workflowManager = workflowManager;
_genericEventService = genericEventService;
_contentManager = contentManager;
}
public ILogger Logger { get; set; }
// This could be invoked by external applications and services to trigger an event within the workflows.
public ActionResult Trigger(string nonce) {
int contentItemId;
string signal;
if (!_genericEventService.DecryptNonce(nonce, out contentItemId, out signal)) {
Logger.Debug("Invalid nonce provided: " + nonce);
return HttpNotFound();
}
var contentItem = _contentManager.Get(contentItemId, VersionOptions.Latest);
if (contentItem == null) {
Logger.Debug("Could not find specified content item in none: " + contentItemId);
return HttpNotFound();
}
// Right now, all workflow instances that are at the WebRequest activity node would continue as soon as a request
// to this action comes in, but that should not happen; it should be controlled by activity configuration and evaluations.
_workflowManager.TriggerEvent(SignalActivity.SignalEventName, contentItem, () => {
var dictionary = new Dictionary<string, object> { { "Content", contentItem }, { SignalActivity.SignalEventName, signal } };
// Let's include query string stuff, so that the WebRequest activity can
// potentially match against certain parameters to decide whether or not it should execute,
// based on its configuration (yet to be defined).
Request.QueryString.CopyTo(dictionary);
Request.Form.CopyTo(dictionary);
return dictionary;
});
// 200
return new EmptyResult();
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Web.Mvc;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Controllers {
public class WorkflowController : Controller {
private readonly IWorkflowManager _workflowManager;
public WorkflowController(IWorkflowManager workflowManager) {
_workflowManager = workflowManager;
}
// This could be invoked by external applications and services to trigger an event within the workflows.
public ActionResult Callback() {
// Right now, all workflow instances that are at the WebRequest activity node would continue as soon as a request
// to this action comes in, but that should not happen; it should be controlled by activity configuration and evaluations.
_workflowManager.TriggerEvent("WebRequest", null, () => {
var dictionary = new Dictionary<string, object>();
// Let's include query string stuff, so that the WebRequest activity can
// potentially match against certain parameters to decide whether or not it should execute,
// based on its configuration (yet to be defined).
Request.QueryString.CopyTo(dictionary);
return dictionary;
});
// A Redirect may have been set by one of the rule events.
if (!string.IsNullOrEmpty(HttpContext.Response.RedirectLocation))
return new EmptyResult();
return new EmptyResult(); // Or maybe an "OK" string. It shouldn't really matter, just as long as we return HTTP 200 OK.
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Linq;
using System.Web.Mvc;
using System.Xml.Linq;
using Newtonsoft.Json;
using Orchard.Data;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
using Orchard.Workflows.Activities;
using Orchard.Workflows.Models;
namespace Orchard.Workflows.Forms {
public class SignalForms : IFormProvider {
private readonly IRepository<ActivityRecord> _activityRecords;
protected dynamic Shape { get; set; }
public Localizer T { get; set; }
public SignalForms(IShapeFactory shapeFactory, IRepository<ActivityRecord> activityRecords) {
_activityRecords = activityRecords;
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
public void Describe(DescribeContext context) {
Func<IShapeFactory, dynamic> form =
shape => {
return Shape.Form(
Id: "SignalEvent",
_Name: Shape.Textbox(
Id: "signal", Name: "Signal",
Title: T("Name of the signal."),
Description: T("The name of the signal."),
Classes: new[] {"textMedium"})
);
};
context.Form("SignalEvent", form);
form =
shape => {
var f = Shape.Form(
Id: "OneOfSignals",
_Parts: Shape.SelectList(
Id: "signal", Name: "Signal",
Title: T("Available signals"),
Description: T("Select a signal."),
Size: 1,
Multiple: false
)
);
var allEvents = _activityRecords
.Table
.Where(x => x.Name == SignalActivity.SignalEventName)
.Select(x => GetState(x.State))
.ToArray()
.Select(x => (string)x.Signal);
foreach (var signal in allEvents) {
f._Parts.Add(new SelectListItem { Value = signal, Text = signal });
}
return f;
};
context.Form("Trigger", form);
}
private dynamic GetState(string state) {
if (!String.IsNullOrWhiteSpace(state)) {
var formatted = JsonConvert.DeserializeXNode(state, "Root").ToString();
var serialized = String.IsNullOrEmpty(formatted) ? "{}" : JsonConvert.SerializeXNode(XElement.Parse(formatted));
return FormParametersHelper.FromJsonString(serialized).Root;
}
return FormParametersHelper.FromJsonString("{}");
}
}
}

View File

@@ -39,12 +39,14 @@ namespace Orchard.Workflows.Forms {
_FormValues: New.Textarea(
Id: "FormValues", Name: "FormValues",
Title: T("Form Values"),
Description: T("For KeyValue, enter one line per key=value pair to submit when using the POST verb. For JSON, enter a valid JSON string"),
Description: T("For KeyValue, enter one line per key=value pair to submit when using the POST verb. For JSon, enter a valid JSon string"),
Classes: new[] {"tokenized"})
);
form._Verb.Add(new SelectListItem { Value = "GET", Text = "GET" });
form._Verb.Add(new SelectListItem { Value = "POST", Text = "POST" });
form._Verb.Add(new SelectListItem { Value = "PUT", Text = "PUT" });
form._Verb.Add(new SelectListItem { Value = "DELETE", Text = "DELETE" });
form._FormFormat.Add(new SelectListItem { Value = "KeyValue", Text = "Key / Value" });
form._FormFormat.Add(new SelectListItem { Value = "Json", Text = "Json" });

View File

@@ -130,6 +130,8 @@
<Compile Include="Activities\BranchActivity.cs" />
<Compile Include="Activities\AssignRoleActivity.cs" />
<Compile Include="Activities\CloseCommentsActivity.cs" />
<Compile Include="Activities\TriggerActivity.cs" />
<Compile Include="Activities\SignalActivity.cs" />
<Compile Include="Activities\MergeBranchActivity.cs" />
<Compile Include="Activities\DeleteActivity.cs" />
<Compile Include="Activities\ExclusiveBranchActivity.cs" />
@@ -138,8 +140,8 @@
<Compile Include="Activities\RedirectActivity.cs" />
<Compile Include="Activities\TimerActivity.cs" />
<Compile Include="Activities\WebRequestActivity.cs" />
<Compile Include="Activities\WebResponseActivity.cs" />
<Compile Include="Controllers\WorkflowController.cs" />
<Compile Include="Controllers\SignalController.cs" />
<Compile Include="Forms\SignalForms.cs" />
<Compile Include="Forms\RedirectActionForm.cs" />
<Compile Include="Forms\TimerForms.cs" />
<Compile Include="Forms\BranchForms.cs" />
@@ -169,6 +171,8 @@
<Compile Include="Forms\UserTaskForms.cs" />
<Compile Include="Forms\NotificationActivityForms.cs" />
<Compile Include="ResourceManifest.cs" />
<Compile Include="Services\GenericEventService.cs" />
<Compile Include="Services\ISignalService.cs" />
<Compile Include="Services\Task.cs" />
<Compile Include="Services\Event.cs" />
<Compile Include="Services\IActivity.cs" />
@@ -176,6 +180,7 @@
<Compile Include="Services\IActivitiesManager.cs" />
<Compile Include="Services\IWorkflowManager.cs" />
<Compile Include="Services\WorkflowManager.cs" />
<Compile Include="Tokens\SignalTokens.cs" />
<Compile Include="ViewModels\AdminEditViewModel.cs" />
<Compile Include="ViewModels\AdminIndexViewModel.cs" />
<Compile Include="ViewModels\WorkflowDefinitionViewModel.cs" />

View File

@@ -150,10 +150,11 @@ var bindForm = function(form, data) {
}
break;
case 'select':
$el.find('option').each(function () {
var self = $(this);
self.attr('selected', values.indexOf(self.attr('value')) != -1);
});
$el.val(values);
//$el.find('option').each(function () {
// var self = $(this);
// self.attr('selected', values.indexOf(self.attr('value')) != -1);
//});
break;
default:
$el.val(val);

View File

@@ -0,0 +1,38 @@
using System;
using System.Text;
using System.Xml.Linq;
using Orchard.Security;
namespace Orchard.Workflows.Services {
public class SignalService : ISignalService {
private readonly IEncryptionService _encryptionService;
public SignalService(IEncryptionService encryptionService) {
_encryptionService = encryptionService;
}
public string CreateNonce(int contentItemId, string signal) {
var challengeToken = new XElement("n", new XAttribute("c", contentItemId), new XAttribute("n", signal)).ToString();
var data = Encoding.UTF8.GetBytes(challengeToken);
return Convert.ToBase64String(_encryptionService.Encode(data));
}
public bool DecryptNonce(string nonce, out int contentItemId, out string signal) {
contentItemId = 0;
signal = "";
try {
var data = _encryptionService.Decode(Convert.FromBase64String(nonce));
var xml = Encoding.UTF8.GetString(data);
var element = XElement.Parse(xml);
contentItemId = Convert.ToInt32(element.Attribute("c").Value);
signal = element.Attribute("n").Value;
return true;
}
catch {
return false;
}
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Orchard.Workflows.Services {
public interface ISignalService : IDependency {
string CreateNonce(int contentItemId, string signal);
bool DecryptNonce(string nonce, out int contentItemId, out string signal);
}
}

View File

@@ -85,6 +85,8 @@ namespace Orchard.Workflows.Services {
Record = awaitingActivityRecord.WorkflowRecord
};
workflowContext.Tokens["Workflow"] = workflowContext;
var activityContext = CreateActivityContext(awaitingActivityRecord.ActivityRecord, tokens);
// check the condition
@@ -109,6 +111,8 @@ namespace Orchard.Workflows.Services {
Tokens = tokens,
};
workflowContext.Tokens["Workflow"] = workflowContext;
var workflowRecord = new WorkflowRecord {
WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord,
State = "{}",

View File

@@ -0,0 +1,48 @@
using System;
using System.Web;
using System.Web.Mvc;
using Orchard.Localization;
using Orchard.Mvc.Extensions;
using Orchard.Tokens;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Tokens {
public class SignalTokens : ITokenProvider {
private readonly IWorkContextAccessor _workContextAccessor;
private readonly Lazy<ISignalService> _signalService;
public SignalTokens(IWorkContextAccessor workContextAccessor, Lazy<ISignalService> signalService) {
_workContextAccessor = workContextAccessor;
_signalService = signalService;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeContext context) {
context.For("Workflow", T("Workflow"), T("Workflow tokens."))
.Token("TriggerUrl:*", T("TriggerUrl:<signal>"), T("The relative url to call in order to trigger the specified Signal."))
;
}
public void Evaluate(EvaluateContext context) {
if (_workContextAccessor.GetContext().HttpContext == null) {
return;
}
context.For<WorkflowContext>("Workflow")
.Token(
token => token.StartsWith("TriggerUrl:", StringComparison.OrdinalIgnoreCase) ? token.Substring("TriggerUrl:".Length) : null,
(token, workflowContext) => {
int contentItemId = 0;
if (workflowContext.Content != null) {
contentItemId = workflowContext.Content.Id;
}
var url = "~/Orchard.Workflows/Signal/Trigger?nonce=" + HttpUtility.UrlEncode(_signalService.Value.CreateNonce(contentItemId, token));
return new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).MakeAbsolute(url);
});
}
}
}