Skip to content

Commit

Permalink
feat: process-modeling templates (#11310)
Browse files Browse the repository at this point in the history
* Streaming used in process modelign

* temp

* GetTemplates tests

* Saveproces from template tests

* Get appversion tests

* use embedded resources for templates to avoid often IO operations on disc

* format

* fix reading template as embedded resource instead as stream

* comments and test added

* docs fix
  • Loading branch information
mirkoSekulic authored Oct 9, 2023
1 parent 35d8e63 commit 85a283e
Show file tree
Hide file tree
Showing 27 changed files with 877 additions and 72 deletions.
9 changes: 9 additions & 0 deletions backend/src/Designer/Controllers/AppDevelopmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using Altinn.Studio.Designer.ViewModels.Response;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -517,5 +518,13 @@ public ActionResult GetOptionListIds(string org, string app)
return NoContent();
}
}

[HttpGet("app-lib-version")]
public VersionResponse GetVersionOfTheAppLib(string org, string app)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var version = _appDevelopmentService.GetAppLibVersion(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer));
return new VersionResponse { Version = version };
}
}
}
54 changes: 33 additions & 21 deletions backend/src/Designer/Controllers/ProcessModelingController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers
Expand All @@ -18,48 +21,57 @@ namespace Altinn.Studio.Designer.Controllers
[Route("designer/api/{org}/{repo:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/process-modelling")]
public class ProcessModelingController : ControllerBase
{
private readonly IAppDevelopmentService _appDevelopmentService;

public ProcessModelingController(IAppDevelopmentService appDevelopmentService)
private readonly IProcessModelingService _processModelingService;
public ProcessModelingController(IProcessModelingService processModelingService)
{
_appDevelopmentService = appDevelopmentService;
_processModelingService = processModelingService;
}

[HttpGet("process-definition")]
public async Task<string> GetProcessDefinition(string org, string repo)
public FileStreamResult GetProcessDefinition(string org, string repo)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
return await _appDevelopmentService.GetProcessDefinition(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer));

Stream processDefinitionStream = _processModelingService.GetProcessDefinitionStream(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer));

return new FileStreamResult(processDefinitionStream, MediaTypeNames.Text.Plain);
}

[HttpPut("process-definition")]
public async Task<IActionResult> SaveProcessDefinition(string org, string repo)
public async Task<IActionResult> SaveProcessDefinition(string org, string repo, CancellationToken cancellationToken)
{
string bpmnFileContent = await ReadRequestBodyContentAsync();

if (bpmnFileContent.Length > 100_000)
{
return BadRequest("BPMN file is too large");
}

Request.EnableBuffering();
try
{
Guard.AssertValidXmlContent(bpmnFileContent);
await Guard.AssertValidXmlStreamAndRewindAsync(Request.Body);
}
catch (ArgumentException)
{
return BadRequest("BPMN file is not valid XML");
}

string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
await _appDevelopmentService.SaveProcessDefinition(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), bpmnFileContent);
return Ok(bpmnFileContent);
await _processModelingService.SaveProcessDefinitionAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), Request.Body, cancellationToken);
return Ok();
}

[HttpGet("templates/{appVersion}")]
public IEnumerable<string> GetTemplates(string org, string repo, Version appVersion)
{
Guard.AssertArgumentNotNull(appVersion, nameof(appVersion));
return _processModelingService.GetProcessDefinitionTemplates(appVersion);
}

private async Task<string> ReadRequestBodyContentAsync()
[HttpPut("templates/{appVersion}/{templateName}")]
public async Task<FileStreamResult> SaveProcessDefinitionFromTemplate(string org, string repo, Version appVersion, string templateName, CancellationToken cancellationToken)
{
using StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8);
return await reader.ReadToEndAsync();
Guard.AssertArgumentNotNull(appVersion, nameof(appVersion));
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer);
await _processModelingService.SaveProcessDefinitionFromTemplateAsync(editingContext, templateName, appVersion, cancellationToken);

Stream processDefinitionStream = _processModelingService.GetProcessDefinitionStream(editingContext);
return new FileStreamResult(processDefinitionStream, MediaTypeNames.Text.Plain);
}
}
}
4 changes: 4 additions & 0 deletions backend/src/Designer/Designer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@
</ItemGroup>


<ItemGroup>
<EmbeddedResource Include="Services\Implementation\ProcessModeling\Templates\**\*.*" />
</ItemGroup>

<Target Name="AfterPublishScript" AfterTargets="Publish">
<MakeDir Directories="$(PublishDir)Templates" Condition="!Exists('$(PublishDir)Templates')" />
<MakeDir Directories="$(PublishDir)Testdata" Condition="!Exists('$(PublishDir)Testdata')" />
Expand Down
27 changes: 27 additions & 0 deletions backend/src/Designer/Helpers/Guard.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;

Expand Down Expand Up @@ -127,5 +128,31 @@ public static void AssertValidXmlContent(string xmlContent)
throw new ArgumentException("Invalid xml content.");
}
}

public static async Task AssertValidXmlStreamAndRewindAsync(Stream xmlStream)
{
XmlReaderSettings settings = new XmlReaderSettings
{
ConformanceLevel = ConformanceLevel.Document,
Async = true
};
try
{
using XmlReader reader = XmlReader.Create(xmlStream, settings);
while (await reader.ReadAsync())
{
// Check if node is formatted correctly
}
}
catch (XmlException)
{
throw new ArgumentException("Invalid xml stream.");
}
finally
{
xmlStream.Seek(0, SeekOrigin.Begin);
}
}

}
}
33 changes: 33 additions & 0 deletions backend/src/Designer/Helpers/PackageVersionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Altinn.Studio.Designer.Helpers
{
public static class PackageVersionHelper
{
public static bool TryGetPackageVersionFromCsprojFile(string csprojFilePath, string packageName, out System.Version version)
{
version = null;
var doc = XDocument.Load(csprojFilePath);
var packageReferences = doc.XPathSelectElements("//PackageReference")
.Where(element => element.Attribute("Include")?.Value == packageName).ToList();

if (packageReferences.Count != 1)
{
return false;
}

var packageReference = packageReferences.First();

string versionString = packageReference.Attribute("Version")?.Value;
if (string.IsNullOrEmpty(versionString))
{
return false;
}

version = System.Version.Parse(versionString);
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -733,42 +733,30 @@ public string[] GetOptionListIds()
}

/// <summary>
/// Saves bpmn file to disk.
/// Saves the processdefinition file on disk.
/// </summary>
/// <param name="file">Content of bpmn file.</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">Throws argument exception if size of syntax validation of the file fails.</exception>
public async Task<string> SaveProcessDefinitionFile(string file, CancellationToken cancellationToken = default)
/// <param name="file">Stream of the file to be saved.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
public async Task SaveProcessDefinitionFileAsync(Stream file, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Guard.AssertNotNullOrEmpty(file, nameof(file));
if (file.Length > 100_000)
if (file.Length > 1000_000)
{
throw new ArgumentException("Bpmn file is too large");
}
await Guard.AssertValidXmlStreamAndRewindAsync(file);

Guard.AssertValidXmlContent(file);

await WriteTextByRelativePathAsync(ProcessDefinitionFilePath, file, true, cancellationToken);
return file;
await WriteStreamByRelativePathAsync(ProcessDefinitionFilePath, file, true, cancellationToken);
}

/// <summary>
/// Gets the Bpmn file from App/config/process/process.bpmn location
/// </summary>
/// <returns>Content of Bpmn file</returns>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <exception cref="NotFoundHttpRequestException">If file doesn't exists.</exception>
public async Task<string> GetProcessDefinitionFile(CancellationToken cancellationToken = default)
public Stream GetProcessDefinitionFile()
{
cancellationToken.ThrowIfCancellationRequested();
if (!FileExistsByRelativePath(ProcessDefinitionFilePath))
{
throw new NotFoundHttpRequestException("Bpmn file not found.");
}

return await ReadTextByRelativePathAsync(ProcessDefinitionFilePath, cancellationToken);
return OpenStreamByRelativePath(ProcessDefinitionFilePath);
}

/// <summary>
Expand Down
24 changes: 20 additions & 4 deletions backend/src/Designer/Infrastructure/GitRepository/GitRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ public async Task<string> ReadTextByRelativePathAsync(string relativeFilePath, C
}
}

/// <summary>
/// Returns fileStream of a file path relative to the repository directory.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <returns>A <see cref="Stream"/>.</returns>
public Stream OpenStreamByRelativePath(string relativeFilePath)
{
string absoluteFilePath = GetAbsoluteFileOrDirectoryPathSanitized(relativeFilePath);

Guard.AssertFilePathWithinParentDirectory(RepositoryDirectory, absoluteFilePath);

return File.OpenRead(absoluteFilePath);
}

/// <summary>
/// Creates a new file or overwrites an existing and writes the text to the specified file path.
/// </summary>
Expand Down Expand Up @@ -168,8 +182,10 @@ public async Task WriteTextByRelativePathAsync(string relativeFilePath, string t
/// <param name="relativeFilePath">File to be created/updated.</param>
/// <param name="stream">Content to be written to the file.</param>
/// <param name="createDirectory">False (default) if you don't want missing directory to be created. True will check if the directory exist and create it if it don't exist.</param>
public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream stream, bool createDirectory = false)
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation in cancelled</param>
public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream stream, bool createDirectory = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Guard.AssertNotNullOrEmpty(relativeFilePath, nameof(relativeFilePath));

string absoluteFilePath = GetAbsoluteFileOrDirectoryPathSanitized(relativeFilePath);
Expand All @@ -181,7 +197,7 @@ public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream
CreateDirectory(absoluteFilePath);
}

await WriteAsync(absoluteFilePath, stream);
await WriteAsync(absoluteFilePath, stream, cancellationToken);
}

/// <summary>
Expand Down Expand Up @@ -378,10 +394,10 @@ private static async Task WriteTextAsync(string absoluteFilePath, string text, C
await sourceStream.WriteAsync(encodedText.AsMemory(0, encodedText.Length), cancellationToken);
}

private static async Task WriteAsync(string absoluteFilePath, Stream stream)
private static async Task WriteAsync(string absoluteFilePath, Stream stream, CancellationToken cancellationToken = default)
{
await using FileStream targetStream = new(absoluteFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true);
await stream.CopyToAsync(targetStream, bufferSize: 4096);
await stream.CopyToAsync(targetStream, bufferSize: 4096, cancellationToken: cancellationToken);
}
}
}
2 changes: 2 additions & 0 deletions backend/src/Designer/Infrastructure/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Altinn.Studio.Designer.Factories;
using Altinn.Studio.Designer.Repository;
using Altinn.Studio.Designer.Services.Implementation;
using Altinn.Studio.Designer.Services.Implementation.ProcessModeling;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -57,6 +58,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddTransient<IAppDevelopmentService, AppDevelopmentService>();
services.AddTransient<IPreviewService, PreviewService>();
services.AddTransient<IResourceRegistry, ResourceRegistryService>();
services.AddTransient<IProcessModelingService, ProcessModelingService>();
services.RegisterDatamodeling(configuration);
services.RegisterUserRequestSynchronization(configuration);

Expand Down
1 change: 1 addition & 0 deletions backend/src/Designer/Models/AltinnRepoContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ protected AltinnRepoContext(string org, string repo)

private void ValidateOrganization(string org)
{
Guard.AssertNotNullOrEmpty(org, nameof(org));
if (!Regex.IsMatch(org, "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$"))
{
throw new ArgumentException("Provided organization name is not valid");
Expand Down
2 changes: 2 additions & 0 deletions backend/src/Designer/Models/AltinnRepoEditingContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text.RegularExpressions;
using Altinn.Studio.Designer.Helpers;

namespace Altinn.Studio.Designer.Models
{
Expand All @@ -22,6 +23,7 @@ private AltinnRepoEditingContext(string org, string repo, string developer) : ba

private static void ValidateDeveloper(string developer)
{
Guard.AssertNotNullOrEmpty(developer, nameof(developer));
if (!Regex.IsMatch(developer, "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$"))
{
throw new ArgumentException("Provided developer name is not valid");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
Expand Down Expand Up @@ -215,20 +218,21 @@ public async Task SaveRuleConfig(AltinnRepoEditingContext altinnRepoEditingConte
}

/// <inheritdoc />
public Task<string> GetProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default)
public Version GetAppLibVersion(AltinnRepoEditingContext altinnRepoEditingContext)
{
cancellationToken.ThrowIfCancellationRequested();
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
return altinnAppGitRepository.GetProcessDefinitionFile(cancellationToken);

}
var csprojFiles = altinnAppGitRepository.FindFiles(new[] { "*.csproj" });

/// <inheritdoc />
public Task<string> SaveProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, string bpmnXml, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
return altinnAppGitRepository.SaveProcessDefinitionFile(bpmnXml, cancellationToken);
foreach (string csprojFile in csprojFiles)
{
if (PackageVersionHelper.TryGetPackageVersionFromCsprojFile(csprojFile, "Altinn.App.Api", out Version version))
{
return version;
}
}

throw new FileNotFoundException("Unable to extract the version of the app-lib from csproj files.");
}
}
}
Loading

0 comments on commit 85a283e

Please sign in to comment.