Skip to content

Commit

Permalink
Revert Multipart form data #329 on PUT endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarne committed Dec 19, 2023
1 parent c018446 commit c8f7dc7
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 379 deletions.
27 changes: 15 additions & 12 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,12 @@ private async Task<ActionResult> CreateAppModelData(
else
{
ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef));
ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);
appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);

if (deserializerResult.HasError)
if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null)
{
return BadRequest(deserializerResult.Error);
return BadRequest(deserializer.Error);
}

appModel = deserializerResult.Model;
}

// runs prefill from repo configuration if config exists
Expand Down Expand Up @@ -622,21 +620,26 @@ private async Task<ActionResult> PutFormData(string org, string app, Instance in
Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]);

ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef));
ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);
object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);

if (!string.IsNullOrEmpty(deserializer.Error))
{
return BadRequest(deserializer.Error);
}

if (deserializerResult.HasError)
if (serviceModel == null)
{
return BadRequest(deserializerResult.Error);
return BadRequest("No data found in content");
}

Dictionary<string, object?>? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessors, deserializerResult.ReportedChanges, _logger);
Dictionary<string, object?>? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessors, _logger);

await UpdatePresentationTextsOnInstance(instance, dataType, deserializerResult.Model);
await UpdateDataValuesOnInstance(instance, dataType, deserializerResult.Model);
await UpdatePresentationTextsOnInstance(instance, dataType, serviceModel);
await UpdateDataValuesOnInstance(instance, dataType, serviceModel);

// Save Formdata to database
DataElement updatedDataElement = await _dataClient.UpdateData(
deserializerResult.Model,
serviceModel,
instanceGuid,
_appModel.GetModelType(classRef),
org,
Expand Down
8 changes: 3 additions & 5 deletions src/Altinn.App.Api/Controllers/InstancesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -971,15 +971,13 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI
}

ModelDeserializer deserializer = new ModelDeserializer(_logger, type);
ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(part.Stream, part.ContentType);
object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType);

if (deserializerResult.HasError)
if (!string.IsNullOrEmpty(deserializer.Error) || data is null)
{
throw new InvalidOperationException(deserializerResult.Error);
throw new InvalidOperationException(deserializer.Error);
}

object data = deserializerResult.Model;

await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data);

await _instantiationProcessor.DataCreation(instance, data, null);
Expand Down
23 changes: 10 additions & 13 deletions src/Altinn.App.Api/Controllers/StatelessDataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,12 @@ public async Task<ActionResult> Get(
await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel);

Instance virtualInstance = new Instance() { InstanceOwner = owner };
await ProcessAllDataWrite(virtualInstance, appModel);
await ProcessAllDataRead(virtualInstance, appModel);

return Ok(appModel);
}

private async Task ProcessAllDataWrite(Instance virtualInstance, object appModel)
private async Task ProcessAllDataRead(Instance virtualInstance, object appModel)
{
foreach (var dataProcessor in _dataProcessors)
{
Expand Down Expand Up @@ -161,7 +161,7 @@ public async Task<ActionResult> GetAnonymous([FromQuery] string dataType)

object appModel = _appModel.Create(classRef);
var virtualInstance = new Instance();
await ProcessAllDataWrite(virtualInstance, appModel);
await ProcessAllDataRead(virtualInstance, appModel);

return Ok(appModel);
}
Expand Down Expand Up @@ -213,15 +213,13 @@ public async Task<ActionResult> Post(
}

ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef));
ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);
object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);

if (deserializerResult.HasError)
if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null)
{
return BadRequest(deserializerResult.Error);
return BadRequest(deserializer.Error);
}

object appModel = deserializerResult.Model;

// runs prefill from repo configuration if config exists
await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel);

Expand Down Expand Up @@ -262,22 +260,21 @@ public async Task<ActionResult> PostAnonymous([FromQuery] string dataType)
}

ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef));
ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);
object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType);

if (deserializerResult.HasError)
if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null)
{
return BadRequest(deserializerResult.Error);
return BadRequest(deserializer.Error);
}

Instance virtualInstance = new Instance();
var appModel = deserializerResult.Model;
foreach (var dataProcessor in _dataProcessors)
{
_logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name);
await dataProcessor.ProcessDataRead(virtualInstance, null, appModel);
}

return Ok(deserializerResult.Model);
return Ok(appModel);
}

private async Task<InstanceOwner?> GetInstanceOwner(string? partyFromHeader)
Expand Down
3 changes: 1 addition & 2 deletions src/Altinn.App.Core/Features/IDataProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ public interface IDataProcessor
/// <param name="instance">Instance that data belongs to</param>
/// <param name="dataId">Data id for the data (nullable if stateless)</param>
/// <param name="data">The data to perform calculations on</param>
/// <param name="changedFields">optional dictionary of field keys and previous values (untrusted from frontend)</param>
public Task ProcessDataWrite(Instance instance, Guid? dataId, object data, Dictionary<string, string?>? changedFields);
public Task ProcessDataWrite(Instance instance, Guid? dataId, object data);
}
4 changes: 2 additions & 2 deletions src/Altinn.App.Core/Helpers/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class JsonHelper
/// <summary>
/// Run DataProcessWrite returning the dictionary of the changed fields.
/// </summary>
public static async Task<Dictionary<string, object?>?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable<IDataProcessor> dataProcessors, Dictionary<string, string?>? changedFields, ILogger logger)
public static async Task<Dictionary<string, object?>?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable<IDataProcessor> dataProcessors, ILogger logger)
{
if (!dataProcessors.Any())
{
Expand All @@ -27,7 +27,7 @@ public static class JsonHelper
foreach (var dataProcessor in dataProcessors)
{
logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", serviceModel.GetType().Name, dataProcessor.GetType().Name);
await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, changedFields);
await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel);
}

string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel);
Expand Down
101 changes: 41 additions & 60 deletions src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;

using Microsoft.Extensions.Logging;

using Newtonsoft.Json;
Expand All @@ -18,6 +19,11 @@ public class ModelDeserializer
private readonly ILogger _logger;
private readonly Type _modelType;

/// <summary>
/// Gets the error message describing what it was that went wrong if there was an issue during deserialization.
/// </summary>
public string? Error { get; private set; }

/// <summary>
/// Initialize a new instance of <see cref="ModelDeserializer"/> with a logger and the Type the deserializer should target.
/// </summary>
Expand All @@ -35,17 +41,14 @@ public ModelDeserializer(ILogger logger, Type modelType)
/// <param name="stream">The data stream to deserialize.</param>
/// <param name="contentType">The content type of the stream.</param>
/// <returns>An instance of the initialized type if deserializing succeed.</returns>
public async Task<ModelDeserializerResult> DeserializeAsync(Stream stream, string? contentType)
public async Task<object?> DeserializeAsync(Stream stream, string? contentType)
{
Error = null;

if (contentType == null)
{
return ModelDeserializerResult.FromError($"Unknown content type \"null\". Cannot read the data.");
}

if (contentType.Contains("multipart/form-data"))
{
return await DeserializeMultipartAsync(stream, contentType);
Error = $"Unknown content type \"null\". Cannot read the data.";
return null;
}

if (contentType.Contains("application/json"))
Expand All @@ -58,74 +61,50 @@ public async Task<ModelDeserializerResult> DeserializeAsync(Stream stream, strin
return await DeserializeXmlAsync(stream);
}

return ModelDeserializerResult.FromError($"Unknown content type {contentType}. Cannot read the data.");
Error = $"Unknown content type {contentType}. Cannot read the data.";
return null;
}

private async Task<ModelDeserializerResult> DeserializeMultipartAsync(Stream stream, string contentType)
private async Task<object?> DeserializeJsonAsync(Stream stream)
{
MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(contentType);
string boundary = mediaType.Boundary.Value!.Trim('"');
var reader = new MultipartReader(boundary, stream);
FormMultipartSection? firstSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection();
if (firstSection?.Name != "dataModel")
{
return ModelDeserializerResult.FromError("First entry in multipart serialization must have name=\"dataModel\"");
}
var modelResult = await DeserializeJsonAsync(firstSection.Section.Body);
if (modelResult.HasError)
{
return modelResult;
}

FormMultipartSection? secondSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection();
Dictionary<string, string?>? reportedChanges = null;
if (secondSection is not null)
{
if (secondSection.Name != "previousValues")
{
return ModelDeserializerResult.FromError("Second entry in multipart serialization must have name=\"previousValues\"");
}
reportedChanges = await System.Text.Json.JsonSerializer.DeserializeAsync<Dictionary<string, string?>>(secondSection.Section.Body);
if (await reader.ReadNextSectionAsync() != null)
{
return ModelDeserializerResult.FromError("Multipart request had more than 2 elements. Only \"dataModel\" and the optional \"previousValues\" are supported.");
}
}
return ModelDeserializerResult.FromSuccess(modelResult.Model, reportedChanges);
}
Error = null;

private async Task<ModelDeserializerResult> DeserializeJsonAsync(Stream stream)
{
try
{
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
string content = await reader.ReadToEndAsync();
return ModelDeserializerResult.FromSuccess(JsonConvert.DeserializeObject(content, _modelType));
return JsonConvert.DeserializeObject(content, _modelType)!;
}
catch (JsonReaderException jsonReaderException)
{
return ModelDeserializerResult.FromError(jsonReaderException.Message);
Error = jsonReaderException.Message;
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected exception when attempting to deserialize JSON into '{modelType}'", _modelType);
return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize JSON into '{_modelType}'");
string message = $"Unexpected exception when attempting to deserialize JSON into '{_modelType}'";
_logger.LogError(ex, message);
Error = message;
return null;
}

}

private async Task<ModelDeserializerResult> DeserializeXmlAsync(Stream stream)
private async Task<object?> DeserializeXmlAsync(Stream stream)
{
// In this first try block we assume that the namespace is the same in the model
// and in the XML. This includes no namespace in both.
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
string? streamContent = await reader.ReadToEndAsync();
Error = null;

string streamContent = null;
try
{
// In this first try block we assume that the namespace is the same in the model
// and in the XML. This includes no namespace in both.
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
streamContent = await reader.ReadToEndAsync();

using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent));
XmlSerializer serializer = new XmlSerializer(_modelType);

return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader));
return serializer.Deserialize(xmlTextReader);
}
catch (InvalidOperationException)
{
Expand All @@ -143,18 +122,21 @@ private async Task<ModelDeserializerResult> DeserializeXmlAsync(Stream stream)
using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent));
XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides);

return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader));
return serializer.Deserialize(xmlTextReader);
}
catch (InvalidOperationException invalidOperationException)
{
// One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different.
return ModelDeserializerResult.FromError($"{invalidOperationException.Message} {invalidOperationException.InnerException?.Message}");
Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException.Message}";
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected exception when attempting to deserialize XML into '{modelType}'", _modelType);
return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize XML into '{_modelType}'");
string message = $"Unexpected exception when attempting to deserialize XML into '{_modelType}'";
_logger.LogError(ex, message);
Error = message;
return null;
}
}

Expand All @@ -175,4 +157,3 @@ private static string GetRootElementName(Type modelType)
}
}
}

Loading

0 comments on commit c8f7dc7

Please sign in to comment.