Skip to content

Commit

Permalink
Implement support for HTTP file download for SendHttpRequest and Writ…
Browse files Browse the repository at this point in the history
…eHttpResponse
  • Loading branch information
sfmskywalker committed Jun 16, 2021
1 parent c6a32d4 commit ebaeabb
Show file tree
Hide file tree
Showing 23 changed files with 170 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ namespace Elsa.Activities.Http
public class SendHttpRequest : Activity
{
private readonly HttpClient _httpClient;
private readonly IEnumerable<IHttpResponseBodyParser> _parsers;
private readonly IEnumerable<IHttpResponseContentReader> _parsers;

public SendHttpRequest(
IHttpClientFactory httpClientFactory,
IEnumerable<IHttpResponseBodyParser> parsers)
IEnumerable<IHttpResponseContentReader> parsers)
{
_httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest));
_parsers = parsers;
Expand Down Expand Up @@ -68,7 +68,7 @@ public class SendHttpRequest : Activity
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The content type to send with the request.",
Options = new[] { "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? ContentType { get; set; }
Expand Down Expand Up @@ -102,7 +102,7 @@ public class SendHttpRequest : Activity
)]
public ICollection<int>? SupportedStatusCodes { get; set; } = new HashSet<int>(new[] { 200 });

[ActivityOutput] public HttpResponseModel Output { get; set; }
[ActivityOutput] public HttpResponseModel? Output { get; set; }

protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
Expand All @@ -122,8 +122,8 @@ protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(Acti

if (hasContent && ReadContent)
{
var formatter = SelectContentParser(contentType!);
responseModel.Content = await formatter.ParseAsync(response, cancellationToken);
var formatter = SelectContentParser(contentType);
responseModel.Content = await formatter.ReadAsync(response, cancellationToken);
}

var statusCode = (int) response.StatusCode;
Expand All @@ -139,21 +139,20 @@ protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(Acti
return Outcomes(outcomes);
}

private IHttpResponseBodyParser SelectContentParser(string contentType)
private IHttpResponseContentReader SelectContentParser(string? contentType)
{
var simpleContentType = contentType?.Split(';').First();
var simpleContentType = contentType?.Split(';').First() ?? "";
var formatters = _parsers.OrderByDescending(x => x.Priority).ToList();
return formatters.FirstOrDefault(
x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)
) ?? formatters.Last();

return formatters.FirstOrDefault(x => x.GetSupportsContentType(simpleContentType)) ?? formatters.Last();
}

private HttpRequestMessage CreateRequest()
{
var method = Method ?? HttpMethods.Get;
var methodSupportsBody = GetMethodSupportsBody(method);
var url = Url;
var request = new HttpRequestMessage(new HttpMethod(Method), url);
var request = new HttpRequestMessage(new HttpMethod(method), url);
var authorizationHeaderValue = Authorization;
var requestHeaders = new HeaderDictionary(RequestHeaders.ToDictionary(x => x.Key, x => new StringValues(x.Value.Split(','))));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Serialization;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.AspNetCore.Http;
Expand All @@ -22,11 +25,13 @@ namespace Elsa.Activities.Http
public class WriteHttpResponse : Activity
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IContentSerializer _contentSerializer;

public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocalizer<WriteHttpResponse> localizer)
public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocalizer<WriteHttpResponse> localizer, IContentSerializer contentSerializer)
{
T = localizer;
_httpContextAccessor = httpContextAccessor;
_contentSerializer = contentSerializer;
}

private IStringLocalizer T { get; }
Expand All @@ -45,10 +50,10 @@ public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocali
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;

/// <summary>
/// The content to send along with the response
/// The content to send along with the response.
/// </summary>
[ActivityInput(Hint = "The HTTP content to write.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Content { get; set; }
public object? Content { get; set; }

/// <summary>
/// The Content-Type header to send along with the response.
Expand All @@ -68,7 +73,7 @@ public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocali
[ActivityInput(
Hint = "The character set to use when writing the response.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "utf-8", "ASCII", "ANSI", "ISO-8859-1" },
Options = new[] { "", "utf-8", "ASCII", "ANSI", "ISO-8859-1" },
DefaultValue = "utf-8",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = PropertyCategories.Advanced)]
Expand All @@ -78,10 +83,10 @@ public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocali
/// The headers to send along with the response.
/// </summary>
[ActivityInput(
Hint = "Additional headers to write.",
Hint = "Additional headers to write.",
UIHint = ActivityInputUIHints.MultiLine,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[]{ SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json },
Category = PropertyCategories.Advanced
)]
public HttpResponseHeaders? ResponseHeaders { get; set; }
Expand All @@ -95,7 +100,7 @@ protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(Acti
return Fault(T["Response has already started"]!);

response.StatusCode = (int) StatusCode;
response.ContentType = $"{ContentType};charset={CharSet}";
response.ContentType = string.IsNullOrWhiteSpace(CharSet) ? ContentType : $"{ContentType};charset={CharSet}";

var headers = ResponseHeaders;

Expand All @@ -105,12 +110,37 @@ protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(Acti
response.Headers[header.Key] = header.Value;
}

var bodyText = Content;

if (!string.IsNullOrWhiteSpace(bodyText))
await response.WriteAsync(bodyText, context.CancellationToken);
await WriteContentAsync(context.CancellationToken);

return Done();
}

private async Task WriteContentAsync(CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext ?? new DefaultHttpContext();
var response = httpContext.Response;

var content = Content;

if (content == null)
return;

if (content is string stringContent)
{
if (!string.IsNullOrWhiteSpace(stringContent))
await response.WriteAsync(stringContent, cancellationToken);

return;
}

if (content is byte[] buffer)
{
await response.Body.WriteAsync(buffer, cancellationToken);
return;
}

var json = _contentSerializer.Serialize(content);
await response.WriteAsync(json, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Elsa.Activities.Http.Liquid;
using Elsa.Activities.Http.Options;
using Elsa.Activities.Http.Parsers;
using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Parsers.Response;
using Elsa.Activities.Http.Services;
using Elsa.Scripting.Liquid.Extensions;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -36,8 +38,9 @@ public static IServiceCollection AddHttpServices(this IServiceCollection service
.AddSingleton<IHttpRequestBodyParser, DefaultHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, JsonHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, FormHttpRequestBodyParser>()
.AddSingleton<IHttpResponseBodyParser, DefaultHttpResponseBodyParser>()
.AddSingleton<IHttpResponseBodyParser, JsonHttpResponseBodyParser>()
.AddSingleton<IHttpResponseContentReader, DefaultHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, JsonHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, FileResponseContentReader>()
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
.AddBookmarkProvider<HttpEndpointBookmarkProvider>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Parsers;
using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Services;
using Elsa.Persistence;
using Elsa.Persistence.Specifications.WorkflowInstances;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Elsa.Activities.Http.Services;
using Microsoft.AspNetCore.Http;

namespace Elsa.Activities.Http.Parsers
namespace Elsa.Activities.Http.Parsers.Request
{
public class DefaultHttpRequestBodyParser : IHttpRequestBodyParser
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;

namespace Elsa.Activities.Http.Parsers.Response
{
public class DefaultHttpResponseContentReader : IHttpResponseContentReader
{
public int Priority => -1;
public bool GetSupportsContentType(string contentType) => true;
public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;

namespace Elsa.Activities.Http.Parsers.Response
{
public class FileResponseContentReader : IHttpResponseContentReader
{
public virtual int Priority => 5;
public virtual bool GetSupportsContentType(string contentType)
{
var types = new[] { "audio", "video", "application", "pdf" };
return types.Any(x => contentType.Contains(x, StringComparison.OrdinalIgnoreCase));
}

public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
using System.Collections.Generic;
using System;
using System.Dynamic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;
using Newtonsoft.Json;

namespace Elsa.Activities.Http.Parsers
namespace Elsa.Activities.Http.Parsers.Response
{
public class JsonHttpResponseBodyParser : IHttpResponseBodyParser
public class JsonHttpResponseContentReader : IHttpResponseContentReader
{
public int Priority => 0;
public IEnumerable<string?> SupportedContentTypes => new[] { "application/json", "text/json" };
public int Priority => 10;
public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase);

public async Task<object> ParseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken)
{
#if NET
var json = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Elsa.Activities.Http.Services
{
public interface IHttpResponseContentReader
{
int Priority { get; }
bool GetSupportsContentType(string contentType);
Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;

namespace Elsa.Scripting.JavaScript.Converters.Jint
{
/// <summary>
/// Prevents Jint from returning byte[] as object[].
/// </summary>
internal class ByteArrayConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
result = JsValue.Null;

if (value is not byte[] buffer)
return false;

result = new ObjectWrapper(engine, buffer);

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;

namespace Elsa.Scripting.JavaScript.Converters
namespace Elsa.Scripting.JavaScript.Converters.Json
{
/// <summary>
/// Ensures that whole numeric values are serialized without any decimal (e.g. 2 instead of 2.0) to ensure deserialization to models having int properties works.
Expand Down

0 comments on commit ebaeabb

Please sign in to comment.