├── .csharpierignore ├── .csharpierrc.yaml ├── renovate.json ├── global.json ├── .config └── dotnet-tools.json ├── .github ├── release.yml └── workflows │ ├── dotnet-test-build.yaml │ └── dotnet-publish.yaml ├── src └── altinn-studio-cli │ ├── Makefile │ ├── Program.cs │ ├── Version │ └── VersionCommand.cs │ ├── Upgrade │ ├── Frontend │ │ └── Fev3Tov4 │ │ │ ├── LayoutRewriter │ │ │ ├── ILayoutMutator.cs │ │ │ ├── Mutators │ │ │ │ ├── AddressMutator.cs │ │ │ │ ├── AttachmentListMutator.cs │ │ │ │ ├── TrbMutator.cs │ │ │ │ ├── GroupMutator.cs │ │ │ │ ├── PropertyCleanupMutator.cs │ │ │ │ ├── RepeatingGroupMutator.cs │ │ │ │ ├── TriggerMutator.cs │ │ │ │ └── LikertMutator.cs │ │ │ ├── LayoutUpgrader.cs │ │ │ └── LayoutMutator.cs │ │ │ ├── SettingsWriter │ │ │ └── SettingsWriter.cs │ │ │ ├── Checks │ │ │ └── Checks.cs │ │ │ ├── IndexFileRewriter │ │ │ └── IndexFileRewriter.cs │ │ │ ├── FooterRewriter │ │ │ └── FooterUpgrader.cs │ │ │ ├── CustomReceiptRewriter │ │ │ └── CustomReceiptUpgrader.cs │ │ │ ├── LayoutSetRewriter │ │ │ └── LayoutSetUpgrader.cs │ │ │ ├── SchemaRefRewriter │ │ │ └── SchemaRefUpgrader.cs │ │ │ └── FrontendUpgrade │ │ │ └── FrontendUpgrade.cs │ ├── UpgradeCommand.cs │ └── Backend │ │ └── v7Tov8 │ │ ├── DockerfileRewriters │ │ ├── Extensions │ │ │ └── StringDockerTagExtensions.cs │ │ └── DockerfileRewriter.cs │ │ ├── CodeRewriters │ │ ├── ModelRewriter.cs │ │ ├── UsingRewriter.cs │ │ ├── TypesRewriter.cs │ │ └── IDataProcessorRewriter.cs │ │ ├── ProjectChecks │ │ └── ProjectChecks.cs │ │ ├── ProjectRewriters │ │ └── ProjectFileRewriter.cs │ │ ├── AppSettingsRewriter │ │ └── AppSettingsRewriter.cs │ │ ├── ProcessRewriter │ │ └── ProcessUpgrader.cs │ │ └── BackendUpgrade │ │ └── BackendUpgrade.cs │ └── altinn-studio-cli.csproj ├── Directory.Packages.props ├── LICENSE ├── README.md ├── Directory.Build.props ├── altinn-studio-cli.sln ├── .gitattributes ├── .editorconfig └── .gitignore /.csharpierignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .git/ 4 | *.xml 5 | *.csproj 6 | *.props 7 | *.targets 8 | -------------------------------------------------------------------------------- /.csharpierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | useTabs: false 3 | tabWidth: 4 4 | endOfLine: auto 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>Altinn/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.411", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "csharpier": { 6 | "version": "1.1.2", 7 | "commands": [ 8 | "csharpier" 9 | ], 10 | "rollForward": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - semver-major 9 | - breaking-change 10 | - title: New Features 🎉 11 | labels: 12 | - semver-minor 13 | - feature 14 | - title: Bugfixes 🐛 15 | labels: 16 | - semver-patch 17 | - bugfix 18 | - title: Dependency Upgrades 📦 19 | labels: 20 | - dependency 21 | - title: Other Changes 22 | labels: 23 | - "*" -------------------------------------------------------------------------------- /src/altinn-studio-cli/Makefile: -------------------------------------------------------------------------------- 1 | # Path: Makefile 2 | 3 | restore: 4 | dotnet restore 5 | 6 | build: restore 7 | dotnet build -c Release --no-restore 8 | 9 | pack: build 10 | dotnet pack -c Release --no-restore --no-build -o publish 11 | 12 | clean: 13 | dotnet clean 14 | rm -rf publish/ 15 | 16 | install-locally: pack 17 | dotnet tool install --global --add-source ./publish Altinn.Studio.Cli --prerelease 18 | 19 | reinstall-locally: uninstall-locally clean pack 20 | dotnet tool install --global --add-source ./publish Altinn.Studio.Cli --prerelease 21 | 22 | uninstall-locally: 23 | dotnet tool uninstall --global Altinn.Studio.Cli -------------------------------------------------------------------------------- /src/altinn-studio-cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using Altinn.Studio.Cli.Upgrade; 3 | using Altinn.Studio.Cli.Version; 4 | 5 | namespace Altinn.Studio.Cli; 6 | 7 | internal sealed class Program 8 | { 9 | private const string RootCommandName = "altinn-studio"; 10 | 11 | static async Task Main(string[] args) 12 | { 13 | var rootCommand = new RootCommand("Command line interface for working with Altinn 3 Applications"); 14 | rootCommand.Name = RootCommandName; 15 | rootCommand.AddCommand(UpgradeCommand.GetUpgradeCommand()); 16 | rootCommand.AddCommand(VersionCommand.GetVersionCommand(RootCommandName)); 17 | await rootCommand.InvokeAsync(args); 18 | return 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-test-build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test on windows, macos and ubuntu 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | types: [opened, synchronize, reopened] 8 | workflow_dispatch: 9 | jobs: 10 | analyze: 11 | strategy: 12 | matrix: 13 | os: [macos-latest,windows-latest,ubuntu-latest] 14 | name: Run dotnet build and test 15 | runs-on: ${{ matrix.os}} 16 | env: 17 | DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE: false 18 | steps: 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 21 | with: 22 | dotnet-version: | 23 | 8.0.x 24 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 25 | with: 26 | fetch-depth: 0 27 | - name: Build 28 | run: | 29 | dotnet build -v m 30 | - name: Test 31 | run: | 32 | dotnet test -v m -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Version/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.Reflection; 3 | 4 | namespace Altinn.Studio.Cli.Version; 5 | 6 | /// 7 | /// Contains the version command 8 | /// 9 | public static class VersionCommand 10 | { 11 | /// 12 | /// Gets the version command 13 | /// 14 | /// 15 | /// 16 | public static Command GetVersionCommand(string executableName) 17 | { 18 | var versionCommand = new Command("version", $"Print version of {executableName} cli"); 19 | versionCommand.SetHandler(() => 20 | { 21 | var version = 22 | Assembly 23 | .GetEntryAssembly() 24 | ?.GetCustomAttribute() 25 | ?.InformationalVersion ?? "Unknown"; 26 | Console.WriteLine($"{executableName} cli v{version}"); 27 | }); 28 | return versionCommand; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/ILayoutMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter; 4 | 5 | internal interface IMutationResult { } 6 | 7 | internal sealed class SkipResult : IMutationResult { } 8 | 9 | internal sealed class DeleteResult : IMutationResult 10 | { 11 | public List Warnings { get; set; } = new List(); 12 | } 13 | 14 | internal sealed class ErrorResult : IMutationResult 15 | { 16 | public required string Message { get; set; } 17 | } 18 | 19 | internal sealed class ReplaceResult : IMutationResult 20 | { 21 | public required JsonObject Component { get; set; } 22 | public List Warnings { get; set; } = new List(); 23 | } 24 | 25 | /** 26 | * Note: The Mutate function receives a clone of the component and can be modified directly, and then returned in ReplaceResult. 27 | */ 28 | 29 | internal interface ILayoutMutator 30 | { 31 | IMutationResult Mutate(JsonObject component, Dictionary componentLookup); 32 | } 33 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/UpgradeCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.BackendUpgrade; 3 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.FrontendUpgrade; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade; 6 | 7 | /// 8 | /// Contains the upgrade command 9 | /// 10 | public static class UpgradeCommand 11 | { 12 | /// 13 | /// Gets the upgrade command 14 | /// 15 | /// 16 | public static Command GetUpgradeCommand() 17 | { 18 | var projectFolderOption = new Option( 19 | name: "--folder", 20 | description: "The project folder to read", 21 | getDefaultValue: () => "CurrentDirectory" 22 | ); 23 | var upgradeCommand = new Command("upgrade", "Upgrade an app") { projectFolderOption }; 24 | upgradeCommand.AddCommand(FrontendUpgrade.GetUpgradeCommand(projectFolderOption)); 25 | upgradeCommand.AddCommand(BackendUpgrade.GetUpgradeCommand(projectFolderOption)); 26 | return upgradeCommand; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Altinn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/AddressMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Rename AddressComponent -> Address 8 | /// 9 | internal sealed class AddressMutator : ILayoutMutator 10 | { 11 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 12 | { 13 | if ( 14 | !component.TryGetPropertyValue("type", out var typeNode) 15 | || typeNode is not JsonValue typeValue 16 | || typeValue.GetValueKind() != JsonValueKind.String 17 | || typeValue.GetValue() is var type && type is null 18 | ) 19 | { 20 | return new ErrorResult() { Message = "Unable to parse component type" }; 21 | } 22 | 23 | if (type == "AddressComponent") 24 | { 25 | component["type"] = "Address"; 26 | return new ReplaceResult() { Component = component }; 27 | } 28 | 29 | return new SkipResult(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Pack and publish nuget tool 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build-pack: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Install dotnet6 17 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | - name: Install deps 22 | run: | 23 | dotnet restore 24 | - name: Build 25 | run: | 26 | dotnet build --configuration Release --no-restore -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} 27 | - name: Pack 28 | run: | 29 | dotnet pack --configuration Release --no-restore --no-build -p:BuildNumber=${{ github.run_number }} -p:Deterministic=true 30 | - name: Versions 31 | run: | 32 | dotnet --version 33 | - name: Publish 34 | run: | 35 | dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # altinn-studio-cli 2 | 3 | > [!IMPORTANT] 4 | > This repository has been archived, development has been moved to [altinn-studio](https://github.com/Altinn/altinn-studio) 5 | 6 | Command line tool for app development 7 | 8 | ## Requirements 9 | 10 | - .NET 8 11 | 12 | ## Local installation 13 | 14 | ### Install from NuGet 15 | To install the tool from NuGet, run the following command: 16 | 17 | ``` 18 | dotnet tool install --global Altinn.Studio.Cli 19 | ``` 20 | 21 | If you already have the tool installed, you can reinstall it by running: 22 | 23 | ``` 24 | dotnet tool update --global Altinn.Studio.Cli 25 | ``` 26 | 27 | ### Install from source 28 | To install the tool from source, run the following command: 29 | 30 | ``` 31 | make install-locally 32 | ``` 33 | 34 | If you already have the tool installed, you can reinstall it by running: 35 | 36 | ``` 37 | make reinstall-locally 38 | ``` 39 | 40 | ## Upgrading apps 41 | 42 | To upgrade an app backend from v7 to v8, navigate to the apps root folder and run the following command: 43 | 44 | ``` 45 | altinn-studio upgrade backend 46 | ``` 47 | 48 | Similarly, to upgrade an app from using frontend v3 to v4, run: 49 | 50 | ``` 51 | altinn-studio upgrade frontend 52 | ``` 53 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | NU1901;NU1902;NU1903;NU1904 5 | 6 | 7 | 8 | latest 9 | true 10 | Recommended 11 | strict 12 | true 13 | all 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.DockerfileRewriters.Extensions; 4 | 5 | /// 6 | /// Extensions for string replacing tags in dockerfiles 7 | /// 8 | internal static class DockerfileStringExtensions 9 | { 10 | /// 11 | /// Replaces the dotnet sdk image tag version in a dockerfile 12 | /// 13 | /// a line in the dockerfile 14 | /// the new image tag 15 | /// 16 | public static string ReplaceSdkVersion(this string line, string imageTag) 17 | { 18 | const string pattern = @"(^FROM mcr.microsoft.com/dotnet/sdk):(.+?)( AS .*)?$"; 19 | return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); 20 | } 21 | 22 | /// 23 | /// Replaces the aspnet image tag version in a dockerfile 24 | /// 25 | /// a line in the dockerfile 26 | /// the new image tag 27 | /// 28 | public static string ReplaceAspNetVersion(this string line, string imageTag) 29 | { 30 | const string pattern = @"(^FROM mcr.microsoft.com/dotnet/aspnet):(.+?)( AS .*)?$"; 31 | return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /altinn-studio-cli.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{618A78E0-3711-453A-A92D-B59B4F3A955F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "altinn-studio-cli", "src\altinn-studio-cli\altinn-studio-cli.csproj", "{3BE8156A-6135-4992-A92B-8DC3891A0DC2}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3BE8156A-6135-4992-A92B-8DC3891A0DC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3BE8156A-6135-4992-A92B-8DC3891A0DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3BE8156A-6135-4992-A92B-8DC3891A0DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3BE8156A-6135-4992-A92B-8DC3891A0DC2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(NestedProjects) = preSolution 25 | {3BE8156A-6135-4992-A92B-8DC3891A0DC2} = {618A78E0-3711-453A-A92D-B59B4F3A955F} 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {CE17B266-7DA2-4590-A664-6CA1A85A43C7} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/LayoutUpgrader.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter; 4 | 5 | internal sealed class LayoutUpgrader 6 | { 7 | private readonly IList _warnings = new List(); 8 | private readonly LayoutMutator _layoutMutator; 9 | private readonly bool _convertGroupTitles; 10 | 11 | public LayoutUpgrader(string uiFolder, bool convertGroupTitles) 12 | { 13 | _layoutMutator = new LayoutMutator(uiFolder); 14 | _convertGroupTitles = convertGroupTitles; 15 | } 16 | 17 | public IList GetWarnings() 18 | { 19 | return _warnings.Concat(_layoutMutator.GetWarnings()).Distinct().ToList(); 20 | } 21 | 22 | /** 23 | * The order of mutators is important, it will do one mutation on all files before moving on to the next 24 | */ 25 | public void Upgrade() 26 | { 27 | _layoutMutator.ReadAllLayoutFiles(); 28 | _layoutMutator.Mutate(new AddressMutator()); 29 | _layoutMutator.Mutate(new LikertMutator()); 30 | _layoutMutator.Mutate(new RepeatingGroupMutator()); 31 | _layoutMutator.Mutate(new GroupMutator()); 32 | _layoutMutator.Mutate(new TriggerMutator()); 33 | _layoutMutator.Mutate(new TrbMutator(_convertGroupTitles)); 34 | _layoutMutator.Mutate(new AttachmentListMutator()); 35 | _layoutMutator.Mutate(new PropertyCleanupMutator()); 36 | } 37 | 38 | public async Task Write() 39 | { 40 | await _layoutMutator.WriteAllLayoutFiles(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.DockerfileRewriters.Extensions; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.DockerfileRewriters; 4 | 5 | /// 6 | /// Rewrites the dockerfile 7 | /// 8 | internal sealed class DockerfileRewriter 9 | { 10 | private readonly string _dockerFilePath; 11 | private readonly string _targetFramework; 12 | 13 | /// 14 | /// Creates a new instance of the class 15 | /// 16 | /// 17 | /// 18 | public DockerfileRewriter(string dockerFilePath, string targetFramework = "net8.0") 19 | { 20 | _dockerFilePath = dockerFilePath; 21 | _targetFramework = targetFramework; 22 | } 23 | 24 | /// 25 | /// Upgrades the dockerfile 26 | /// 27 | public async Task Upgrade() 28 | { 29 | var dockerFile = await File.ReadAllLinesAsync(_dockerFilePath); 30 | var newDockerFile = new List(); 31 | foreach (var line in dockerFile) 32 | { 33 | var imageTag = GetImageTagFromFrameworkVersion(_targetFramework); 34 | newDockerFile.Add(line.ReplaceSdkVersion(imageTag).ReplaceAspNetVersion(imageTag)); 35 | } 36 | 37 | await File.WriteAllLinesAsync(_dockerFilePath, newDockerFile); 38 | } 39 | 40 | private static string GetImageTagFromFrameworkVersion(string targetFramework) 41 | { 42 | return targetFramework switch 43 | { 44 | "net6.0" => "6.0-alpine", 45 | "net7.0" => "7.0-alpine", 46 | _ => "8.0-alpine", 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/altinn-studio-cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | Altinn.Studio.Cli 6 | enable 7 | enable 8 | Altinn.Studio.Cli 9 | true 10 | altinn-studio 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) 27 | preview.0 28 | v 29 | true 30 | 31 | 32 | 33 | 34 | $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | true 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/CodeRewriters/ModelRewriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.CodeRewriters; 6 | 7 | /// 8 | /// Rewrites the Model classes to include Guid AltinnRowId 9 | /// 10 | internal sealed class ModelRewriter : CSharpSyntaxRewriter 11 | { 12 | private readonly List _modelsInList; 13 | 14 | public ModelRewriter(List modelsInList) 15 | { 16 | _modelsInList = modelsInList; 17 | } 18 | 19 | /// 20 | public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) 21 | { 22 | if ( 23 | _modelsInList.Contains(node.Identifier.Text) 24 | && !node.Members.Any(m => m is PropertyDeclarationSyntax p && p.Identifier.Text == "AltinnRowId") 25 | ) 26 | { 27 | var altinnRowIdProperty = 28 | SyntaxFactory.ParseMemberDeclaration( 29 | """ 30 | [XmlAttribute("altinnRowId")] 31 | [JsonPropertyName("altinnRowId")] 32 | [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 33 | public Guid AltinnRowId { get; set; } 34 | """ 35 | ) ?? throw new InvalidOperationException("Failed to parse AltinnRowId property declaration"); 36 | 37 | var altinnRowIdShouldSerialize = 38 | SyntaxFactory.ParseMemberDeclaration( 39 | """ 40 | public bool ShouldSerializeAltinnRowId() 41 | { 42 | return AltinnRowId != default; 43 | } 44 | """ 45 | ) 46 | ?? throw new InvalidOperationException("Failed to parse ShouldSerializeAltinnRowId method declaration"); 47 | 48 | altinnRowIdProperty = altinnRowIdProperty.WithTrailingTrivia( 49 | SyntaxFactory.LineFeed, 50 | SyntaxFactory.LineFeed 51 | ); 52 | altinnRowIdShouldSerialize = altinnRowIdShouldSerialize.WithTrailingTrivia( 53 | SyntaxFactory.LineFeed, 54 | SyntaxFactory.LineFeed 55 | ); 56 | 57 | node = node.WithMembers(node.Members.InsertRange(0, [altinnRowIdProperty, altinnRowIdShouldSerialize])); 58 | } 59 | 60 | return base.VisitClassDeclaration(node); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/AttachmentListMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Upgrades AttachmentList component 8 | /// 9 | internal sealed class AttachmentListMutator : ILayoutMutator 10 | { 11 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 12 | { 13 | if ( 14 | !component.TryGetPropertyValue("type", out var typeNode) 15 | || typeNode is not JsonValue typeValue 16 | || typeValue.GetValueKind() != JsonValueKind.String 17 | || typeValue.GetValue() is var type && type is null 18 | ) 19 | { 20 | return new ErrorResult() { Message = "Unable to parse component type" }; 21 | } 22 | 23 | if (type == "AttachmentList") 24 | { 25 | // Remove includePDF and update dataTypeIds if includePDF is true 26 | if (component.TryGetPropertyValue("includePDF", out var includePDFNode)) 27 | { 28 | component.Remove("includePDF"); 29 | if (includePDFNode is JsonValue includePDFValue && includePDFValue.GetValueKind() == JsonValueKind.True) 30 | { 31 | if ( 32 | component.TryGetPropertyValue("dataTypeIds", out var dataTypeIdsNode1) 33 | && dataTypeIdsNode1 is JsonArray dataTypeIdsArray1 34 | ) 35 | { 36 | dataTypeIdsArray1.Add(JsonValue.Create("ref-data-as-pdf")); 37 | } 38 | else 39 | { 40 | component["dataTypeIds"] = new JsonArray() { JsonValue.Create("ref-data-as-pdf") }; 41 | } 42 | return new ReplaceResult() { Component = component }; 43 | } 44 | } 45 | 46 | // If dataTypeIds is undefined, null or empty, set to current-task 47 | if ( 48 | !component.TryGetPropertyValue("dataTypeIds", out var dataTypeIdsNode2) 49 | || dataTypeIdsNode2 is not JsonArray dataTypeIdsArray2 50 | || dataTypeIdsArray2.Count == 0 51 | ) 52 | { 53 | component["dataTypeIds"] = new JsonArray() { JsonValue.Create("current-task") }; 54 | } 55 | return new ReplaceResult() { Component = component }; 56 | } 57 | 58 | return new SkipResult(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/TrbMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Upgrades title/description text resource bindings for Groups and Repeating groups 8 | /// Assumes that Group components have already been upgraded to new Group components 9 | /// 10 | internal sealed class TrbMutator : ILayoutMutator 11 | { 12 | private readonly bool _convertGroupTitles; 13 | 14 | public TrbMutator(bool convertGroupTitles) 15 | { 16 | _convertGroupTitles = convertGroupTitles; 17 | } 18 | 19 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 20 | { 21 | if ( 22 | !component.TryGetPropertyValue("type", out var typeNode) 23 | || typeNode is not JsonValue typeValue 24 | || typeValue.GetValueKind() != JsonValueKind.String 25 | || typeValue.GetValue() is var type && type is null 26 | ) 27 | { 28 | return new ErrorResult() { Message = "Unable to parse component type" }; 29 | } 30 | 31 | // Change body to description for group 32 | if ( 33 | type == "Group" 34 | && component.TryGetPropertyValue("textResourceBindings", out var groupTrbNode) 35 | && groupTrbNode is JsonObject groupTrbObject 36 | && groupTrbObject.TryGetPropertyValue("body", out var groupBodyNode) 37 | ) 38 | { 39 | groupTrbObject["description"] = groupBodyNode?.DeepClone(); 40 | groupTrbObject.Remove("body"); 41 | return new ReplaceResult() { Component = component }; 42 | } 43 | 44 | // Change body to description and title to summary title for repeating group 45 | if ( 46 | type == "RepeatingGroup" 47 | && component.TryGetPropertyValue("textResourceBindings", out var repeatingTrbNode) 48 | && repeatingTrbNode is JsonObject repeatingTrbObject 49 | ) 50 | { 51 | if (repeatingTrbObject.TryGetPropertyValue("body", out var repeatingBodyNode)) 52 | { 53 | repeatingTrbObject["description"] = repeatingBodyNode?.DeepClone(); 54 | repeatingTrbObject.Remove("body"); 55 | } 56 | 57 | if (_convertGroupTitles && repeatingTrbObject.TryGetPropertyValue("title", out var repeatingTitleNode)) 58 | { 59 | repeatingTrbObject["summaryTitle"] = repeatingTitleNode?.DeepClone(); 60 | repeatingTrbObject.Remove("title"); 61 | } 62 | 63 | return new ReplaceResult() { Component = component }; 64 | } 65 | 66 | return new SkipResult(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/ProjectChecks/ProjectChecks.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.ProjectChecks; 4 | 5 | /// 6 | /// Checks the project file for unsupported versions 7 | /// 8 | internal sealed class ProjectChecks 9 | { 10 | private readonly XDocument _doc; 11 | 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// 16 | public ProjectChecks(string projectFilePath) 17 | { 18 | var xmlString = File.ReadAllText(projectFilePath); 19 | _doc = XDocument.Parse(xmlString); 20 | } 21 | 22 | /// 23 | /// Verifies that the project is using supported versions of Altinn.App.Api and Altinn.App.Core 24 | /// 25 | /// 26 | public bool SupportedSourceVersion() 27 | { 28 | var altinnAppCoreElements = GetAltinnAppCoreElement(); 29 | var altinnAppApiElements = GetAltinnAppApiElement(); 30 | if (altinnAppCoreElements is null || altinnAppApiElements is null) 31 | { 32 | return false; 33 | } 34 | 35 | if ( 36 | altinnAppApiElements 37 | .Select(apiElement => apiElement.Attribute("Version")?.Value) 38 | .Any(altinnAppApiVersion => !SupportedSourceVersion(altinnAppApiVersion)) 39 | ) 40 | { 41 | return false; 42 | } 43 | 44 | return altinnAppCoreElements 45 | .Select(coreElement => coreElement.Attribute("Version")?.Value) 46 | .All(altinnAppCoreVersion => SupportedSourceVersion(altinnAppCoreVersion)); 47 | } 48 | 49 | private List? GetAltinnAppCoreElement() 50 | { 51 | return _doc 52 | .Root?.Elements("ItemGroup") 53 | .Elements("PackageReference") 54 | .Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core") 55 | .ToList(); 56 | } 57 | 58 | private List? GetAltinnAppApiElement() 59 | { 60 | return _doc 61 | .Root?.Elements("ItemGroup") 62 | .Elements("PackageReference") 63 | .Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api") 64 | .ToList(); 65 | } 66 | 67 | /// 68 | /// Check that version is >=7.0.0 69 | /// 70 | /// 71 | /// 72 | private bool SupportedSourceVersion(string? version) 73 | { 74 | if (version is null) 75 | { 76 | return false; 77 | } 78 | 79 | var versionParts = version.Split('.'); 80 | if (versionParts.Length < 3) 81 | { 82 | return false; 83 | } 84 | 85 | if (int.TryParse(versionParts[0], out int major) && major >= 7) 86 | { 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/GroupMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Upgrades groups with / without panel 8 | /// 9 | internal sealed class GroupMutator : ILayoutMutator 10 | { 11 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 12 | { 13 | if ( 14 | !component.TryGetPropertyValue("type", out var typeNode) 15 | || typeNode is not JsonValue typeValue 16 | || typeValue.GetValueKind() != JsonValueKind.String 17 | || typeValue.GetValue() is var type && type is null 18 | ) 19 | { 20 | return new ErrorResult() { Message = "Unable to parse component type" }; 21 | } 22 | 23 | if ( 24 | type == "Group" 25 | && ( 26 | !component.ContainsKey("maxCount") 27 | || component.TryGetPropertyValue("maxCount", out var maxCountNode) 28 | && maxCountNode is JsonValue maxCountValue 29 | && maxCountValue.GetValueKind() == JsonValueKind.Number 30 | && maxCountValue.GetValue() <= 1 31 | ) 32 | ) 33 | { 34 | if (component.ContainsKey("maxCount")) 35 | { 36 | component.Remove("maxCount"); 37 | } 38 | if (component.TryGetPropertyValue("panel", out var panelNode)) 39 | { 40 | // if panel has reference, delete the entire component and log warning 41 | if (panelNode is JsonObject panelObject && panelObject.ContainsKey("groupReference")) 42 | { 43 | return new DeleteResult() 44 | { 45 | Warnings = new List() 46 | { 47 | "Group with panel and groupReference is not supported in v4, deleting component", 48 | }, 49 | }; 50 | } 51 | 52 | // Change panel to new groupingIndicator 53 | component.Remove("panel"); 54 | component["groupingIndicator"] = "panel"; 55 | } 56 | 57 | // Change old showGroupingIndicator to new groupingIndicator 58 | if (component.TryGetPropertyValue("showGroupingIndicator", out var showGroupingIndicatorNode)) 59 | { 60 | component.Remove("showGroupingIndicator"); 61 | if ( 62 | showGroupingIndicatorNode is JsonValue showGroupingIndicatorValue 63 | && showGroupingIndicatorValue.GetValueKind() == JsonValueKind.True 64 | ) 65 | { 66 | component["groupingIndicator"] = "indented"; 67 | } 68 | } 69 | 70 | return new ReplaceResult() { Component = component }; 71 | } 72 | 73 | return new SkipResult(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/SettingsWriter/SettingsWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.SettingsWriter; 6 | 7 | /// 8 | /// Creates basic Settings.json file for all layout sets that are missing one 9 | /// Must be run after LayoutSetUpgrader 10 | /// 11 | internal sealed class SettingsCreator 12 | { 13 | private readonly IList _warnings = new List(); 14 | private readonly string _uiFolder; 15 | private readonly Dictionary _settingsCollection = new(); 16 | 17 | public SettingsCreator(string uiFolder) 18 | { 19 | _uiFolder = uiFolder; 20 | } 21 | 22 | public IList GetWarnings() 23 | { 24 | return _warnings; 25 | } 26 | 27 | public void Upgrade() 28 | { 29 | var layoutSets = Directory.GetDirectories(_uiFolder); 30 | foreach (var layoutSet in layoutSets) 31 | { 32 | var settingsFileName = Path.Combine(layoutSet, "Settings.json"); 33 | if (File.Exists(settingsFileName)) 34 | { 35 | continue; 36 | } 37 | 38 | var layoutsFolder = Path.Combine(layoutSet, "layouts"); 39 | if (!Directory.Exists(layoutsFolder)) 40 | { 41 | var compactFilePath = string.Join( 42 | Path.DirectorySeparatorChar, 43 | layoutSet.Split(Path.DirectorySeparatorChar)[^2..] 44 | ); 45 | _warnings.Add($"No layouts folder found in layoutset {compactFilePath}, skipping"); 46 | continue; 47 | } 48 | 49 | var order = Directory 50 | .GetFiles(layoutsFolder, "*.json") 51 | .Select(f => $@"""{Path.GetFileNameWithoutExtension(f)}""") 52 | .ToList(); 53 | 54 | var layoutSettingsJsonString = 55 | $@"{{""$schema"": ""https://altinncdn.no/schemas/json/layout/layoutSettings.schema.v1.json"", ""pages"": {{""order"": [{string.Join(", ", order)}]}}}}"; 56 | 57 | var parsedJson = JsonNode.Parse(layoutSettingsJsonString) 58 | ?? throw new InvalidOperationException($"Failed to parse generated JSON string: {layoutSettingsJsonString}"); 59 | 60 | _settingsCollection.Add(settingsFileName, parsedJson); 61 | } 62 | } 63 | 64 | public async Task Write() 65 | { 66 | JsonSerializerOptions options = new JsonSerializerOptions 67 | { 68 | WriteIndented = true, 69 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 70 | }; 71 | 72 | await Task.WhenAll( 73 | _settingsCollection.Select(async settingsTuple => 74 | { 75 | settingsTuple.Deconstruct(out var filePath, out var settingsJson); 76 | 77 | var settingsText = settingsJson.ToJsonString(options); 78 | await File.WriteAllTextAsync(filePath, settingsText); 79 | }) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/Checks/Checks.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.Checks; 5 | 6 | /// 7 | /// Checks for known issues in the app 8 | /// 9 | internal sealed class Checker 10 | { 11 | private readonly IList _warnings = new List(); 12 | private readonly string _textsFolder; 13 | 14 | public Checker(string textsFolder) 15 | { 16 | _textsFolder = textsFolder; 17 | } 18 | 19 | public void CheckTextDataModelReferences() 20 | { 21 | var textResourceFiles = Directory.GetFiles(_textsFolder, "*.json"); 22 | foreach (var textResourceFile in textResourceFiles) 23 | { 24 | var compactFilePath = string.Join( 25 | Path.DirectorySeparatorChar, 26 | textResourceFile.Split(Path.DirectorySeparatorChar)[^2..] 27 | ); 28 | var textResourceNode = JsonNode.Parse( 29 | File.ReadAllText(textResourceFile), 30 | null, 31 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 32 | ); 33 | if ( 34 | textResourceNode is JsonObject textResourceObject 35 | && textResourceObject.TryGetPropertyValue("resources", out var resourcesNode) 36 | && resourcesNode is JsonArray resourcesArray 37 | ) 38 | { 39 | foreach (var resourceNode in resourcesArray) 40 | { 41 | if ( 42 | resourceNode is JsonObject resourceObject 43 | && resourceObject.TryGetPropertyValue("variables", out var variablesNode) 44 | && variablesNode is JsonArray variablesArray 45 | ) 46 | { 47 | foreach (var variableNode in variablesArray) 48 | { 49 | if ( 50 | variableNode is JsonObject variableObject 51 | && variableObject.TryGetPropertyValue("dataSource", out var dataSourceNode) 52 | && dataSourceNode is JsonValue dataSourceValue 53 | && dataSourceValue.GetValueKind() == JsonValueKind.String 54 | && dataSourceValue.GetValue() == "dataModel.default" 55 | ) 56 | { 57 | _warnings.Add( 58 | $@"Found ""dataSource"": ""dataModel.default"" in {compactFilePath}, this is not recommended in v4. Please consider referring to a specific data model instead." 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | else 66 | { 67 | _warnings.Add($"Unable to parse {compactFilePath}, skipping check"); 68 | } 69 | } 70 | } 71 | 72 | public IList GetWarnings() 73 | { 74 | return _warnings.Distinct().ToList(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/IndexFileRewriter/IndexFileRewriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.IndexFileRewriter; 4 | 5 | internal sealed class IndexFileUpgrader 6 | { 7 | private readonly IList _warnings = new List(); 8 | private readonly string _indexFile; 9 | private readonly string _version; 10 | private string? _indexHtml = null; 11 | 12 | public IndexFileUpgrader(string indexFile, string version) 13 | { 14 | _indexFile = indexFile; 15 | _version = version; 16 | } 17 | 18 | public IList GetWarnings() 19 | { 20 | return _warnings; 21 | } 22 | 23 | public void Upgrade() 24 | { 25 | try 26 | { 27 | var lines = File.ReadLines(_indexFile); 28 | var newLines = new List(); 29 | 30 | foreach (var line in lines) 31 | { 32 | // Deleting fortawesome and fonts 33 | if ( 34 | Regex.IsMatch(line, @".*.*") 35 | || Regex.IsMatch(line, @".*.*") 36 | || Regex.IsMatch(line, @".*https://altinncdn\.no/toolkits/fortawesome.*") 37 | || Regex.IsMatch(line, @".*https://altinncdn\.no/fonts.*") 38 | ) 39 | { 40 | continue; 41 | } 42 | 43 | // Replace css bundle 44 | if (Regex.IsMatch(line, @".*altinn-app-frontend\.css.*")) 45 | { 46 | // This preserves indentation 47 | newLines.Add( 48 | Regex.Replace( 49 | line, 50 | @"" 52 | ) 53 | ); 54 | continue; 55 | } 56 | 57 | // Replace js bundle 58 | if (Regex.IsMatch(line, @".*altinn-app-frontend\.js.*")) 59 | { 60 | // This preserves indentation 61 | newLines.Add( 62 | Regex.Replace( 63 | line, 64 | @"" 66 | ) 67 | ); 68 | continue; 69 | } 70 | 71 | newLines.Add(line); 72 | } 73 | 74 | // Remove excessive newlines 75 | _indexHtml = Regex.Replace(string.Join("\n", newLines), @"(\s*\n){3,}", "\n\n"); 76 | } 77 | catch (Exception e) 78 | { 79 | _warnings.Add($"Failed to rewrite Index.cshtml: {e.Message}"); 80 | } 81 | } 82 | 83 | public async Task Write() 84 | { 85 | if (_indexHtml is null) 86 | { 87 | return; 88 | } 89 | await File.WriteAllTextAsync(_indexFile, _indexHtml); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/FooterRewriter/FooterUpgrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.FooterRewriter; 6 | 7 | /// 8 | /// Fixes footer accessibility link if present, if not adds a default footer 9 | /// Should be run before SchemaRefUpgrader 10 | /// 11 | internal sealed class FooterUpgrader 12 | { 13 | private readonly IList _warnings = new List(); 14 | private JsonNode? _footer; 15 | private readonly string _uiFolder; 16 | 17 | public FooterUpgrader(string uiFolder) 18 | { 19 | _uiFolder = uiFolder; 20 | } 21 | 22 | public IList GetWarnings() 23 | { 24 | return _warnings; 25 | } 26 | 27 | public void Upgrade() 28 | { 29 | var footerFile = Path.Combine(_uiFolder, "footer.json"); 30 | if (File.Exists(footerFile)) 31 | { 32 | var footerJson = JsonNode.Parse( 33 | File.ReadAllText(footerFile), 34 | null, 35 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 36 | ); 37 | if ( 38 | footerJson is JsonObject footerJsonObject 39 | && footerJsonObject.TryGetPropertyValue("footer", out var footerNode) 40 | && footerNode is JsonArray footerArray 41 | ) 42 | { 43 | foreach (var footerItem in footerArray) 44 | { 45 | if ( 46 | footerItem is JsonObject footerItemObject 47 | && footerItemObject.TryGetPropertyValue("type", out var typeNode) 48 | && typeNode is JsonValue typeValue 49 | && typeValue.GetValueKind() == JsonValueKind.String 50 | && typeValue.GetValue() == "Link" 51 | && footerItemObject.TryGetPropertyValue("target", out var targetNode) 52 | && targetNode is JsonValue targetValue 53 | && targetValue.GetValueKind() == JsonValueKind.String 54 | && targetValue.GetValue() == "https://www.altinn.no/om-altinn/tilgjengelighet/" 55 | ) 56 | { 57 | footerItemObject["target"] = JsonValue.Create("general.accessibility_url"); 58 | } 59 | } 60 | _footer = footerJson; 61 | } 62 | else 63 | { 64 | _warnings.Add($"Unable to parse footer.json, skipping footer upgrade"); 65 | } 66 | } 67 | else 68 | { 69 | _footer = JsonNode.Parse( 70 | @"{ ""$schema"": ""https://altinncdn.no/schemas/json/layout/footer.schema.v1.json"", ""footer"": [ { ""type"": ""Link"", ""icon"": ""information"", ""title"": ""general.accessibility"", ""target"": ""general.accessibility_url"" } ] }" 71 | ); 72 | } 73 | } 74 | 75 | public async Task Write() 76 | { 77 | if (_footer is null) 78 | { 79 | return; 80 | } 81 | 82 | JsonSerializerOptions options = new JsonSerializerOptions 83 | { 84 | WriteIndented = true, 85 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 86 | }; 87 | 88 | var footerText = _footer.ToJsonString(options); 89 | await File.WriteAllTextAsync(Path.Combine(_uiFolder, "footer.json"), footerText); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/PropertyCleanupMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Cleans up properties that are no longer allowed 8 | /// 9 | internal sealed class PropertyCleanupMutator : ILayoutMutator 10 | { 11 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 12 | { 13 | if ( 14 | !component.TryGetPropertyValue("type", out var typeNode) 15 | || typeNode is not JsonValue typeValue 16 | || typeValue.GetValueKind() != JsonValueKind.String 17 | || typeValue.GetValue() is var type && type is null 18 | ) 19 | { 20 | return new ErrorResult() { Message = "Unable to parse component type" }; 21 | } 22 | 23 | var formComponentTypes = new List() 24 | { 25 | "Address", 26 | "Checkboxes", 27 | "Custom", 28 | "Datepicker", 29 | "Dropdown", 30 | "FileUpload", 31 | "FileUploadWithTag", 32 | "Grid", 33 | "Input", 34 | "Likert", 35 | "List", 36 | "Map", 37 | "MultipleSelect", 38 | "RadioButtons", 39 | "TextArea", 40 | }; 41 | 42 | if (component.ContainsKey("componentType")) 43 | { 44 | component.Remove("componentType"); 45 | } 46 | 47 | if (component.ContainsKey("triggers")) 48 | { 49 | component.Remove("triggers"); 50 | } 51 | 52 | if (component.ContainsKey("textResourceId")) 53 | { 54 | component.Remove("textResourceId"); 55 | } 56 | 57 | if (component.ContainsKey("customType")) 58 | { 59 | component.Remove("customType"); 60 | } 61 | 62 | if (component.ContainsKey("description")) 63 | { 64 | component.Remove("description"); 65 | } 66 | 67 | if (component.ContainsKey("pageRef")) 68 | { 69 | component.Remove("pageRef"); 70 | } 71 | 72 | // All non-form components 73 | if (!formComponentTypes.Contains(type)) 74 | { 75 | if (component.ContainsKey("required")) 76 | { 77 | component.Remove("required"); 78 | } 79 | 80 | if (component.ContainsKey("readOnly")) 81 | { 82 | component.Remove("readOnly"); 83 | } 84 | } 85 | 86 | if ( 87 | type != "RepeatingGroup" 88 | && !formComponentTypes.Contains(type) 89 | && component.ContainsKey("dataModelBindings") 90 | ) 91 | { 92 | component.Remove("dataModelBindings"); 93 | } 94 | 95 | if ( 96 | (type == "FileUpload" || type == "FileUploadWithTag") 97 | && component.TryGetPropertyValue("dataModelBindings", out var fileDmb) 98 | && fileDmb is JsonObject fileDmbObject 99 | && fileDmbObject.Count == 0 100 | ) 101 | { 102 | component.Remove("dataModelBindings"); 103 | } 104 | 105 | if (type == "Paragraph" && component.ContainsKey("size")) 106 | { 107 | component.Remove("size"); 108 | } 109 | 110 | if (type == "Panel" && component.ContainsKey("size")) 111 | { 112 | component.Remove("size"); 113 | } 114 | 115 | if (type == "NavigationBar" && component.ContainsKey("textResourceBindings")) 116 | { 117 | component.Remove("textResourceBindings"); 118 | } 119 | 120 | return new ReplaceResult() { Component = component }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Xml; 3 | using System.Xml.Linq; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.ProjectRewriters; 6 | 7 | /// 8 | /// Upgrade the csproj file 9 | /// 10 | internal sealed class ProjectFileRewriter 11 | { 12 | private readonly XDocument _doc; 13 | private readonly string _projectFilePath; 14 | private readonly string _targetVersion; 15 | private readonly string _targetFramework; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | /// 22 | /// 23 | public ProjectFileRewriter( 24 | string projectFilePath, 25 | string targetVersion = "8.0.0", 26 | string targetFramework = "net8.0" 27 | ) 28 | { 29 | _projectFilePath = projectFilePath; 30 | _targetVersion = targetVersion; 31 | var xmlString = File.ReadAllText(projectFilePath); 32 | _doc = XDocument.Parse(xmlString); 33 | _targetFramework = targetFramework; 34 | } 35 | 36 | /// 37 | /// Upgrades and writes an upgraded version of the project file to disk 38 | /// 39 | public async Task Upgrade() 40 | { 41 | var altinnAppCoreElements = GetAltinnAppCoreElement(); 42 | altinnAppCoreElements?.ForEach(c => c.Attribute("Version")?.SetValue(_targetVersion)); 43 | 44 | var altinnAppApiElements = GetAltinnAppApiElement(); 45 | altinnAppApiElements?.ForEach(a => a.Attribute("Version")?.SetValue(_targetVersion)); 46 | 47 | IgnoreWarnings("1591", "1998"); // Require xml doc and await in async methods 48 | 49 | GetTargetFrameworkElement()?.ForEach(t => t.SetValue(_targetFramework)); 50 | 51 | await Save(); 52 | } 53 | 54 | private void IgnoreWarnings(params string[] warnings) 55 | { 56 | var noWarn = _doc.Root?.Elements("PropertyGroup").Elements("NoWarn").ToList(); 57 | switch (noWarn?.Count) 58 | { 59 | case 0: 60 | _doc.Root?.Elements("PropertyGroup") 61 | .First() 62 | .Add(new XElement("NoWarn", "$(NoWarn);" + string.Join(';', warnings))); 63 | break; 64 | 65 | case 1: 66 | var valueElement = noWarn[0]; 67 | foreach (var warning in warnings) 68 | { 69 | if (!valueElement.Value.Contains(warning)) 70 | { 71 | valueElement.SetValue($"{valueElement.Value};{warning}"); 72 | } 73 | } 74 | 75 | break; 76 | } 77 | } 78 | 79 | private List? GetAltinnAppCoreElement() 80 | { 81 | return _doc 82 | .Root?.Elements("ItemGroup") 83 | .Elements("PackageReference") 84 | .Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core") 85 | .ToList(); 86 | } 87 | 88 | private List? GetAltinnAppApiElement() 89 | { 90 | return _doc 91 | .Root?.Elements("ItemGroup") 92 | .Elements("PackageReference") 93 | .Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api") 94 | .ToList(); 95 | } 96 | 97 | private List? GetTargetFrameworkElement() 98 | { 99 | return _doc.Root?.Elements("PropertyGroup").Elements("TargetFramework").ToList(); 100 | } 101 | 102 | private async Task Save() 103 | { 104 | XmlWriterSettings xws = new XmlWriterSettings(); 105 | xws.Async = true; 106 | xws.OmitXmlDeclaration = true; 107 | xws.Indent = true; 108 | xws.Encoding = Encoding.UTF8; 109 | await using XmlWriter xw = XmlWriter.Create(_projectFilePath, xws); 110 | await _doc.WriteToAsync(xw, CancellationToken.None); 111 | await xw.FlushAsync(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/CodeRewriters/UsingRewriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.CodeRewriters; 6 | 7 | /// 8 | /// Rewrite the usings of moved interfaces 9 | /// 10 | internal sealed class UsingRewriter : CSharpSyntaxRewriter 11 | { 12 | private const string CommonInterfaceNamespace = "Altinn.App.Core.Interface"; 13 | 14 | private readonly Dictionary _usingMappings = new Dictionary() 15 | { 16 | { "IAppEvents", "Altinn.App.Core.Internal.App" }, 17 | { "IApplication", "Altinn.App.Core.Internal.App" }, 18 | { "IAppResources", "Altinn.App.Core.Internal.App" }, 19 | { "IAuthenticationClient", "Altinn.App.Core.Internal.Auth" }, 20 | { "IAuthorizationClient", "Altinn.App.Core.Internal.Auth" }, 21 | { "IDataClient", "Altinn.App.Core.Internal.Data" }, 22 | { "IPersonClient", "Altinn.App.Core.Internal.Registers" }, 23 | { "IOrganizationClient", "Altinn.App.Core.Internal.Registers" }, 24 | { "IEventsClient", "Altinn.App.Core.Internal.Events" }, 25 | { "IInstanceClient", "Altinn.App.Core.Internal.Instances" }, 26 | { "IInstanceEventClient", "Altinn.App.Core.Internal.Instances" }, 27 | { "IPrefill", "Altinn.App.Core.Internal.Prefill" }, 28 | { "IProcessClient", "Altinn.App.Core.Internal.Process" }, 29 | { "IProfileClient", "Altinn.App.Core.Internal.Profile" }, 30 | { "IAltinnPartyClient", "Altinn.App.Core.Internal.Registers" }, 31 | { "ISecretsClient", "Altinn.App.Core.Internal.Secrets" }, 32 | { "ITaskEvents", "Altinn.App.Core.Internal.Process" }, 33 | { "IUserTokenProvider", "Altinn.App.Core.Internal.Auth" }, 34 | }; 35 | 36 | /// 37 | public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) 38 | { 39 | foreach (var mapping in _usingMappings) 40 | { 41 | if (HasFieldOfType(node, mapping.Key)) 42 | { 43 | node = AddUsing(node, mapping.Value); 44 | } 45 | } 46 | 47 | if (ImplementsIProcessExclusiveGateway(node)) 48 | { 49 | node = AddUsing(node, "Altinn.App.Core.Models.Process"); 50 | } 51 | 52 | return RemoveOldUsing(node); 53 | } 54 | 55 | private bool HasFieldOfType(CompilationUnitSyntax node, string typeName) 56 | { 57 | var fieldDecendants = node.DescendantNodes().OfType(); 58 | return fieldDecendants.Any(f => f.Declaration.Type.ToString() == typeName); 59 | } 60 | 61 | private bool ImplementsIProcessExclusiveGateway(CompilationUnitSyntax node) 62 | { 63 | var classDecendants = node.DescendantNodes().OfType(); 64 | return classDecendants.Any(c => 65 | c.BaseList?.Types.Any(t => t.Type.ToString() == "IProcessExclusiveGateway") == true 66 | ); 67 | } 68 | 69 | private CompilationUnitSyntax AddUsing(CompilationUnitSyntax node, string usingString) 70 | { 71 | if (HasUsingDefined(node, usingString)) 72 | { 73 | return node; 74 | } 75 | var usingName = SyntaxFactory.ParseName(usingString); 76 | var usingDirective = SyntaxFactory 77 | .UsingDirective(usingName) 78 | .NormalizeWhitespace() 79 | .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); 80 | return node.AddUsings(usingDirective); 81 | } 82 | 83 | private bool HasUsingDefined(CompilationUnitSyntax node, string usingName) 84 | { 85 | var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); 86 | return usingDirectiveSyntaxes.Any(u => u.Name?.ToString() == usingName); 87 | } 88 | 89 | private CompilationUnitSyntax? RemoveOldUsing(CompilationUnitSyntax node) 90 | { 91 | var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); 92 | var usingDirectiveSyntax = usingDirectiveSyntaxes.FirstOrDefault(u => 93 | u.Name?.ToString() == CommonInterfaceNamespace 94 | ); 95 | if (usingDirectiveSyntax is not null) 96 | { 97 | return node.RemoveNode(usingDirectiveSyntax, SyntaxRemoveOptions.KeepNoTrivia); 98 | } 99 | return node; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/RepeatingGroupMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Upgrades repeating groups to new repeating group component 8 | /// This assumes that likert has already been converted to new likert component 9 | /// 10 | internal sealed class RepeatingGroupMutator : ILayoutMutator 11 | { 12 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 13 | { 14 | var warnings = new List(); 15 | if ( 16 | !component.TryGetPropertyValue("type", out var typeNode) 17 | || typeNode is not JsonValue typeValue 18 | || typeValue.GetValueKind() != JsonValueKind.String 19 | || typeValue.GetValue() is var type && type is null 20 | ) 21 | { 22 | return new ErrorResult() { Message = "Unable to parse component type" }; 23 | } 24 | 25 | if ( 26 | type == "Group" 27 | // Check for maxCount > 1 28 | && component.TryGetPropertyValue("maxCount", out var maxCountNode) 29 | && maxCountNode is JsonValue maxCountValue 30 | && maxCountValue.GetValueKind() == JsonValueKind.Number 31 | && maxCountValue.GetValue() > 1 32 | ) 33 | { 34 | component["type"] = "RepeatingGroup"; 35 | 36 | // Convert and warn about filter property if present 37 | if ( 38 | component.TryGetPropertyValue("edit", out var editNode) 39 | && editNode is JsonObject editObject 40 | && editObject.TryGetPropertyValue("filter", out var filterNode) 41 | ) 42 | { 43 | editObject.Remove("filter"); 44 | 45 | if (!component.ContainsKey("hiddenRow")) 46 | { 47 | // Convert filter to hiddenRow 48 | if (filterNode is JsonArray filterArray && filterArray.Count > 0) 49 | { 50 | var expressions = new List(); 51 | 52 | foreach (var filterItem in filterArray) 53 | { 54 | if ( 55 | filterItem is JsonObject filterObject 56 | && filterObject.TryGetPropertyValue("key", out var keyNode) 57 | && filterObject.TryGetPropertyValue("value", out var valueNode) 58 | && keyNode is JsonValue keyValue 59 | && keyValue.GetValueKind() == JsonValueKind.String 60 | && valueNode is JsonValue valueValue 61 | && valueValue.GetValueKind() == JsonValueKind.String 62 | && keyValue.GetValue() is var key 63 | && valueValue.GetValue() is var value 64 | ) 65 | { 66 | expressions.Add(@$"[""notEquals"", [""dataModel"", ""{key}""], ""{value}""]"); 67 | } 68 | } 69 | 70 | if (expressions.Count == 1) 71 | { 72 | component["hiddenRow"] = JsonNode.Parse(expressions[0]); 73 | } 74 | else if (expressions.Count > 1) 75 | { 76 | component["hiddenRow"] = JsonNode.Parse(@$"[""or"", {string.Join(", ", expressions)}]"); 77 | } 78 | 79 | if (expressions.Count == filterArray.Count) 80 | { 81 | warnings.Add( 82 | "filter property has been migrated to hiddenRow property, please verify that the component is still working as intended" 83 | ); 84 | } 85 | else if (expressions.Count > 0) 86 | { 87 | warnings.Add( 88 | "filter property was partially migrated to hiddenRow property, please verify that the component is still working as intended" 89 | ); 90 | } 91 | else 92 | { 93 | warnings.Add( 94 | "filter property could not be migrated to hiddenRow property, something went wrong" 95 | ); 96 | } 97 | } 98 | } 99 | else 100 | { 101 | warnings.Add( 102 | "filter property could not be migrated to hiddenRow property, because the component already has a hiddenRow property" 103 | ); 104 | } 105 | } 106 | 107 | return new ReplaceResult() { Component = component, Warnings = warnings }; 108 | } 109 | 110 | return new SkipResult(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.AppSettingsRewriter; 5 | 6 | /// 7 | /// Rewrites the appsettings.*.json files 8 | /// 9 | internal sealed class AppSettingsRewriter 10 | { 11 | /// 12 | /// The pattern used to search for appsettings.*.json files 13 | /// 14 | public const string AppSettingsFilePattern = "appsettings*.json"; 15 | 16 | private readonly Dictionary _appSettingsJsonCollection; 17 | 18 | private readonly IList _warnings = new List(); 19 | 20 | private readonly JsonDocumentOptions _jsonDocumentOptions = new JsonDocumentOptions() 21 | { 22 | CommentHandling = JsonCommentHandling.Skip, 23 | AllowTrailingCommas = true, 24 | }; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | public AppSettingsRewriter(string appSettingsFolder) 30 | { 31 | _appSettingsJsonCollection = new Dictionary(); 32 | foreach (var file in Directory.GetFiles(appSettingsFolder, AppSettingsFilePattern)) 33 | { 34 | var json = File.ReadAllText(file); 35 | var appSettingsJson = JsonNode.Parse(json, null, _jsonDocumentOptions); 36 | if (appSettingsJson is not JsonObject appSettingsJsonObject) 37 | { 38 | _warnings.Add($"Unable to parse AppSettings file {file} as a json object, skipping"); 39 | continue; 40 | } 41 | 42 | this._appSettingsJsonCollection.Add(file, appSettingsJsonObject); 43 | } 44 | } 45 | 46 | /// 47 | /// Gets the warnings 48 | /// 49 | public IList GetWarnings() 50 | { 51 | return _warnings; 52 | } 53 | 54 | /// 55 | /// Upgrades the appsettings.*.json files 56 | /// 57 | public void Upgrade() 58 | { 59 | foreach ((var fileName, var appSettingsJson) in _appSettingsJsonCollection) 60 | { 61 | RewriteRemoveHiddenDataSetting(fileName, appSettingsJson); 62 | } 63 | } 64 | 65 | /// 66 | /// Writes the appsettings.*.json files 67 | /// 68 | public async Task Write() 69 | { 70 | var tasks = _appSettingsJsonCollection.Select(async appSettingsFiles => 71 | { 72 | appSettingsFiles.Deconstruct(out var fileName, out var appSettingsJson); 73 | 74 | JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true }; 75 | await File.WriteAllTextAsync(fileName, appSettingsJson.ToJsonString(options)); 76 | }); 77 | 78 | await Task.WhenAll(tasks); 79 | } 80 | 81 | private void RewriteRemoveHiddenDataSetting(string fileName, JsonObject settings) 82 | { 83 | try 84 | { 85 | // Look for "AppSettings" object 86 | settings.TryGetPropertyValue("AppSettings", out var appSettingsNode); 87 | if (appSettingsNode is not JsonObject appSettingsObject) 88 | { 89 | // No "AppSettings" object found, nothing to change 90 | return; 91 | } 92 | 93 | // Look for "RemoveHiddenDataPreview" property 94 | appSettingsObject.TryGetPropertyValue("RemoveHiddenDataPreview", out var removeHiddenDataPreviewNode); 95 | 96 | if (removeHiddenDataPreviewNode is not JsonValue removeHiddenDataPreviewValue) 97 | { 98 | // No "RemoveHiddenDataPreview" property found, nothing to change 99 | return; 100 | } 101 | 102 | // Get value of "RemoveHiddenDataPreview" property 103 | if (!removeHiddenDataPreviewValue.TryGetValue(out var removeHiddenDataValue)) 104 | { 105 | _warnings.Add( 106 | $"RemoveHiddenDataPreview has unexpected value {removeHiddenDataPreviewValue.ToJsonString()} in {fileName}, expected a boolean" 107 | ); 108 | return; 109 | } 110 | 111 | appSettingsObject.Remove("RemoveHiddenDataPreview"); 112 | if (appSettingsObject.ContainsKey("RemoveHiddenData")) 113 | { 114 | _warnings.Add( 115 | $"RemoveHiddenData already exists in AppSettings, skipping. Tool would have set the value to: {removeHiddenDataValue} in {fileName}" 116 | ); 117 | } 118 | else 119 | { 120 | appSettingsObject.Add("RemoveHiddenData", removeHiddenDataValue); 121 | } 122 | 123 | if (appSettingsObject.ContainsKey("RequiredValidation")) 124 | { 125 | _warnings.Add( 126 | $"RequiredValidation already exists in AppSettings, skipping. Tool would have set the value to: {removeHiddenDataValue} in {fileName}" 127 | ); 128 | } 129 | else 130 | { 131 | appSettingsObject.Add("RequiredValidation", removeHiddenDataValue); 132 | } 133 | } 134 | catch (Exception e) 135 | { 136 | _warnings.Add( 137 | $"Unable to parse appsettings file {fileName}, error: {e.Message}. Skipping upgrade of RemoveHiddenDataPreview for this file" 138 | ); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/TriggerMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Upgrades trigger property 8 | /// Should be run after group component and address mutations 9 | /// 10 | internal sealed class TriggerMutator : ILayoutMutator 11 | { 12 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 13 | { 14 | if ( 15 | !component.TryGetPropertyValue("type", out var typeNode) 16 | || typeNode is not JsonValue typeValue 17 | || typeValue.GetValueKind() != JsonValueKind.String 18 | || typeValue.GetValue() is var type && type is null 19 | ) 20 | { 21 | return new ErrorResult() { Message = "Unable to parse component type" }; 22 | } 23 | 24 | JsonNode? triggersNode = null; 25 | 26 | var formComponentTypes = new List() 27 | { 28 | "Address", 29 | "Checkboxes", 30 | "Custom", 31 | "Datepicker", 32 | "Dropdown", 33 | "FileUpload", 34 | "FileUploadWithTag", 35 | "Grid", 36 | "Input", 37 | "Likert", 38 | "List", 39 | "Map", 40 | "MultipleSelect", 41 | "RadioButtons", 42 | "TextArea", 43 | }; 44 | if (formComponentTypes.Contains(type)) 45 | { 46 | // "showValidations": ["AllExceptRequired"] is now default in v4, so no additional changes are needed. 47 | if (component.TryGetPropertyValue("triggers", out triggersNode)) 48 | { 49 | component.Remove("triggers"); 50 | } 51 | 52 | // Removing redundant "showValidations": ["AllExceptRequired"] from previous upgrade runs. 53 | if ( 54 | component.TryGetPropertyValue("showValidations", out var showValidationsNode) 55 | && showValidationsNode is JsonArray showValidationsArray 56 | && showValidationsArray 57 | .Where(x => x is JsonValue && x.GetValueKind() == JsonValueKind.String) 58 | .Select(x => x?.GetValue()) 59 | is var showValidationsValues 60 | && showValidationsValues.Count() == 1 61 | && showValidationsValues.Contains("AllExceptRequired") 62 | ) 63 | { 64 | component.Remove("showValidations"); 65 | } 66 | 67 | return new ReplaceResult() { Component = component }; 68 | } 69 | 70 | if (type == "RepeatingGroup" && component.TryGetPropertyValue("triggers", out triggersNode)) 71 | { 72 | component.Remove("triggers"); 73 | 74 | if ( 75 | triggersNode is JsonArray triggersArray 76 | && triggersArray 77 | .Where(x => x is JsonValue && x.GetValueKind() == JsonValueKind.String) 78 | .Select(x => x?.GetValue()) 79 | is var triggers 80 | && (triggers.Contains("validation") || triggers.Contains("validateRow")) 81 | ) 82 | { 83 | component.Add("validateOnSaveRow", JsonNode.Parse(@"[""All""]")); 84 | return new ReplaceResult() { Component = component }; 85 | } 86 | } 87 | 88 | if (type == "NavigationButtons" && component.TryGetPropertyValue("triggers", out triggersNode)) 89 | { 90 | component.Remove("triggers"); 91 | 92 | if ( 93 | triggersNode is JsonArray triggersArray 94 | && triggersArray 95 | .Where(x => x is JsonValue && x.GetValueKind() == JsonValueKind.String) 96 | .Select(x => x?.GetValue()) 97 | is var triggers 98 | ) 99 | { 100 | if (triggers.Contains("validatePage")) 101 | { 102 | component.Add("validateOnNext", JsonNode.Parse(@"{""page"": ""current"", ""show"": [""All""]}")); 103 | return new ReplaceResult() { Component = component }; 104 | } 105 | 106 | if (triggers.Contains("validateAllPages")) 107 | { 108 | component.Add("validateOnNext", JsonNode.Parse(@"{""page"": ""all"", ""show"": [""All""]}")); 109 | return new ReplaceResult() { Component = component }; 110 | } 111 | 112 | if (triggers.Contains("validateCurrentAndPreviousPages")) 113 | { 114 | component.Add( 115 | "validateOnNext", 116 | JsonNode.Parse(@"{""page"": ""currentAndPrevious"", ""show"": [""All""]}") 117 | ); 118 | return new ReplaceResult() { Component = component }; 119 | } 120 | } 121 | } 122 | 123 | if (type == "NavigationBar" && component.TryGetPropertyValue("triggers", out triggersNode)) 124 | { 125 | component.Remove("triggers"); 126 | 127 | if ( 128 | triggersNode is JsonArray triggersArray 129 | && triggersArray 130 | .Where(x => x is JsonValue && x.GetValueKind() == JsonValueKind.String) 131 | .Select(x => x?.GetValue()) 132 | is var triggers 133 | ) 134 | { 135 | if (triggers.Contains("validatePage")) 136 | { 137 | component.Add("validateOnForward", JsonNode.Parse(@"{""page"": ""current"", ""show"": [""All""]}")); 138 | return new ReplaceResult() { Component = component }; 139 | } 140 | 141 | if (triggers.Contains("validateAllPages")) 142 | { 143 | component.Add("validateOnForward", JsonNode.Parse(@"{""page"": ""all"", ""show"": [""All""]}")); 144 | return new ReplaceResult() { Component = component }; 145 | } 146 | 147 | if (triggers.Contains("validateCurrentAndPreviousPages")) 148 | { 149 | component.Add( 150 | "validateOnForward", 151 | JsonNode.Parse(@"{""page"": ""currentAndPrevious"", ""show"": [""All""]}") 152 | ); 153 | return new ReplaceResult() { Component = component }; 154 | } 155 | } 156 | } 157 | 158 | return new SkipResult(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/Mutators/LikertMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter.Mutators; 5 | 6 | /// 7 | /// Converts Group + Likert to new Likert component 8 | /// 9 | internal sealed class LikertMutator : ILayoutMutator 10 | { 11 | public IMutationResult Mutate(JsonObject component, Dictionary componentLookup) 12 | { 13 | if ( 14 | !component.TryGetPropertyValue("type", out var typeNode) 15 | || typeNode is not JsonValue typeValue 16 | || typeValue.GetValueKind() != JsonValueKind.String 17 | || typeValue.GetValue() is var type && type is null 18 | ) 19 | { 20 | return new ErrorResult() { Message = "Unable to parse component type" }; 21 | } 22 | 23 | // Delete old likert component 24 | if ( 25 | type == "Likert" 26 | && ( 27 | !component.TryGetPropertyValue("dataModelBindings", out var oldLikertDmbNode) 28 | || oldLikertDmbNode is not JsonObject oldLikertDmbObject 29 | || !oldLikertDmbObject.ContainsKey(("questions")) && !oldLikertDmbObject.ContainsKey(("answer")) 30 | ) 31 | ) 32 | { 33 | return new DeleteResult(); 34 | } 35 | 36 | if ( 37 | type == "Group" 38 | // Check for maxCount > 1 39 | && component.TryGetPropertyValue("maxCount", out var maxCountNode) 40 | && maxCountNode is JsonValue maxCountValue 41 | && maxCountValue.GetValueKind() == JsonValueKind.Number 42 | && maxCountValue.GetValue() > 1 43 | // Check for edit.mode == "likert" 44 | && component.TryGetPropertyValue("edit", out var editNode) 45 | && editNode is JsonObject editObject 46 | && editObject.TryGetPropertyValue("mode", out var modeNode) 47 | && modeNode is JsonValue modeValue 48 | && modeValue.GetValueKind() == JsonValueKind.String 49 | && modeValue.GetValue() == "likert" 50 | ) 51 | { 52 | component["type"] = "Likert"; 53 | component.Remove("maxCount"); 54 | 55 | // Move filter from edit to root 56 | if (editObject.TryGetPropertyValue("filter", out var filterNode)) 57 | { 58 | component["filter"] = filterNode?.DeepClone(); 59 | } 60 | component.Remove("edit"); 61 | 62 | // Change group binding to questions 63 | if ( 64 | !component.TryGetPropertyValue("dataModelBindings", out var groupDmbNode) 65 | || groupDmbNode is not JsonObject groupDmbObject 66 | || !groupDmbObject.TryGetPropertyValue("group", out var groupNode) 67 | ) 68 | { 69 | return new ErrorResult() { Message = "Group (likert) is missing dataModelBindings.group" }; 70 | } 71 | groupDmbObject.Add("questions", groupNode?.DeepClone()); 72 | groupDmbObject.Remove("group"); 73 | 74 | // Find id of likert component from children 75 | if ( 76 | !component.TryGetPropertyValue("children", out var childrenNode) 77 | || childrenNode is not JsonArray childrenArray 78 | || childrenArray.Count != 1 79 | || childrenArray[0] is not JsonValue childIdValue 80 | || childIdValue.GetValueKind() != JsonValueKind.String 81 | || childIdValue.GetValue() is var childId && childId is null 82 | ) 83 | { 84 | return new ErrorResult() 85 | { 86 | Message = "Group (likert) has invalid children, expected array with one string", 87 | }; 88 | } 89 | 90 | // Find (old) likert component from lookup 91 | if (!componentLookup.TryGetValue(childId, out var likertComponent)) 92 | { 93 | return new ErrorResult() { Message = $"Unable to find likert component with id {childId}" }; 94 | } 95 | 96 | component.Remove("children"); 97 | 98 | // Move textResourceBindings.title from likert to textResourceBindings.questions in group 99 | if ( 100 | likertComponent.TryGetPropertyValue(("textResourceBindings"), out var likertTrbNode) 101 | && likertTrbNode is JsonObject likertTrbObject 102 | && likertTrbObject.TryGetPropertyValue("title", out var questionsTrb) 103 | ) 104 | { 105 | if ( 106 | !component.TryGetPropertyValue("textResourceBindings", out var groupTrbNode) 107 | || groupTrbNode is not JsonObject groupTrbObject 108 | ) 109 | { 110 | groupTrbObject = new JsonObject(); 111 | component["textResourceBindings"] = groupTrbObject; 112 | } 113 | groupTrbObject.Add("questions", questionsTrb?.DeepClone()); 114 | } 115 | 116 | // Move dataModelBindings.simpleBinding from likert to group 117 | if ( 118 | !likertComponent.TryGetPropertyValue("dataModelBindings", out var likertDmbNode) 119 | || likertDmbNode is not JsonObject likertDmbObject 120 | || !likertDmbObject.TryGetPropertyValue("simpleBinding", out var simpleBindingNode) 121 | ) 122 | { 123 | return new ErrorResult() { Message = "Likert is missing dataModelBindings.simpleBinding" }; 124 | } 125 | groupDmbObject.Add("answer", simpleBindingNode?.DeepClone()); 126 | 127 | // Move standard properties from likert to group 128 | if (likertComponent.TryGetPropertyValue("options", out var optionsNode)) 129 | { 130 | component["options"] = optionsNode?.DeepClone(); 131 | } 132 | if (likertComponent.TryGetPropertyValue("optionsId", out var optionsIdNode)) 133 | { 134 | component["optionsId"] = optionsIdNode?.DeepClone(); 135 | } 136 | if (likertComponent.TryGetPropertyValue("secure", out var secureNode)) 137 | { 138 | component["secure"] = secureNode?.DeepClone(); 139 | } 140 | if (likertComponent.TryGetPropertyValue("sortOrder", out var sortOrderNode)) 141 | { 142 | component["sortOrder"] = sortOrderNode?.DeepClone(); 143 | } 144 | if (likertComponent.TryGetPropertyValue("source", out var sourceNode)) 145 | { 146 | component["source"] = sourceNode?.DeepClone(); 147 | } 148 | if (likertComponent.TryGetPropertyValue("required", out var requiredNode)) 149 | { 150 | component["required"] = requiredNode?.DeepClone(); 151 | } 152 | if (likertComponent.TryGetPropertyValue("readOnly", out var readOnlyNode)) 153 | { 154 | component["readOnly"] = readOnlyNode?.DeepClone(); 155 | } 156 | // Note: triggers are converted later 157 | if (likertComponent.TryGetPropertyValue("triggers", out var triggersNode)) 158 | { 159 | component["triggers"] = triggersNode?.DeepClone(); 160 | } 161 | 162 | return new ReplaceResult() { Component = component }; 163 | } 164 | 165 | return new SkipResult(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/CustomReceiptRewriter/CustomReceiptUpgrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.CustomReceiptRewriter; 6 | 7 | /// 8 | /// Moves receiptLayout into its own layout set. 9 | /// Must be run after LayoutSetUpgrader 10 | /// 11 | internal sealed class CustomReceiptUpgrader 12 | { 13 | private readonly IList _warnings = new List(); 14 | private readonly string _uiFolder; 15 | private readonly string _receiptLayoutSetName; 16 | private string? _receiptLayoutName; 17 | private string? _receiptLayoutPath; 18 | 19 | private JsonNode? _layoutSets; 20 | private readonly Dictionary _settingsCollection = new(); 21 | 22 | public CustomReceiptUpgrader(string uiFolder, string receiptLayoutSetName) 23 | { 24 | _uiFolder = uiFolder; 25 | _receiptLayoutSetName = receiptLayoutSetName; 26 | } 27 | 28 | public IList GetWarnings() 29 | { 30 | return _warnings; 31 | } 32 | 33 | public void Upgrade() 34 | { 35 | string? oldLayoutSetId = null; 36 | var layoutSetDirectories = Directory.GetDirectories(_uiFolder); 37 | foreach (var layoutSet in layoutSetDirectories) 38 | { 39 | var settingsFileName = Path.Combine(layoutSet, "Settings.json"); 40 | if (File.Exists(settingsFileName)) 41 | { 42 | // Try to find a receiptLayout in settings 43 | var settingsNode = JsonNode.Parse(File.ReadAllText(settingsFileName)); 44 | if ( 45 | settingsNode is JsonObject settingsObject 46 | && settingsObject.TryGetPropertyValue("receiptLayoutName", out var receiptLayoutNameNode) 47 | && receiptLayoutNameNode is JsonValue receiptLayoutNameValue 48 | ) 49 | { 50 | // Use the first receiptLayout found, and make sure to remove all others as well 51 | if (string.IsNullOrEmpty(_receiptLayoutName)) 52 | { 53 | _receiptLayoutName = receiptLayoutNameValue.GetValue(); 54 | oldLayoutSetId = Path.GetFileName(layoutSet); 55 | _receiptLayoutPath = Path.Combine(layoutSet, "layouts", $"{_receiptLayoutName}.json"); 56 | } 57 | else 58 | { 59 | var compactSettingsFilePath = string.Join( 60 | Path.DirectorySeparatorChar, 61 | settingsFileName.Split(Path.DirectorySeparatorChar)[^2..] 62 | ); 63 | if (_receiptLayoutPath is null) 64 | throw new InvalidOperationException("Receipt layout path is unexpectedly null"); 65 | var compactReceiptFilePath = string.Join( 66 | Path.DirectorySeparatorChar, 67 | _receiptLayoutPath.Split(Path.DirectorySeparatorChar)[^3..] 68 | ); 69 | _warnings.Add( 70 | $"Found additional receiptLayoutName in {compactSettingsFilePath}. Currently using {compactReceiptFilePath}." 71 | ); 72 | } 73 | settingsObject.Remove("receiptLayoutName"); 74 | _settingsCollection.Add(settingsFileName, settingsNode); 75 | } 76 | } 77 | } 78 | 79 | if (string.IsNullOrEmpty(_receiptLayoutName)) 80 | { 81 | return; 82 | } 83 | 84 | if (!File.Exists(_receiptLayoutPath)) 85 | { 86 | if (_receiptLayoutPath is null) 87 | throw new InvalidOperationException("Receipt layout path is unexpectedly null"); 88 | var compactReceiptFilePath = string.Join( 89 | Path.DirectorySeparatorChar, 90 | _receiptLayoutPath.Split(Path.DirectorySeparatorChar)[^3..] 91 | ); 92 | _warnings.Add($"Receipt layout file {compactReceiptFilePath} does not exist, skipping upgrade."); 93 | return; 94 | } 95 | 96 | // Add new layout-set for receiptLayout 97 | var layoutSetsNode = JsonNode.Parse(File.ReadAllText(Path.Combine(_uiFolder, "layout-sets.json"))); 98 | if (layoutSetsNode is JsonObject layoutSetsObject) 99 | { 100 | layoutSetsObject.TryGetPropertyValue("sets", out var setsNode); 101 | 102 | if (setsNode is not JsonArray setsArray) 103 | { 104 | _warnings.Add("layout-sets.json is missing 'sets' array"); 105 | return; 106 | } 107 | 108 | // Find out what dataType to use 109 | string? dataType = null; 110 | foreach (var setNode in setsArray) 111 | { 112 | if ( 113 | setNode is JsonObject setObject 114 | && setObject.TryGetPropertyValue("id", out var idNode) 115 | && idNode is JsonValue idValue 116 | && idValue.GetValue() == oldLayoutSetId 117 | ) 118 | { 119 | setObject.TryGetPropertyValue("dataType", out var dataTypeNode); 120 | if (dataTypeNode is JsonValue dataTypeValue) 121 | { 122 | dataType = dataTypeValue.GetValue(); 123 | break; 124 | } 125 | } 126 | } 127 | 128 | if (dataType is null) 129 | { 130 | _warnings.Add("Could not find dataType for custom receipt, skipping upgrade."); 131 | return; 132 | } 133 | 134 | setsArray.Add( 135 | JsonNode.Parse( 136 | $@"{{""id"": ""{_receiptLayoutSetName}"", ""dataType"": ""{dataType}"", ""tasks"": [""CustomReceipt""]}}" 137 | ) 138 | ); 139 | 140 | _layoutSets = layoutSetsObject; 141 | } 142 | else 143 | { 144 | _warnings.Add("layout-sets.json is not a valid JSON object"); 145 | } 146 | } 147 | 148 | public async Task Write() 149 | { 150 | if (_receiptLayoutName is not null && _layoutSets is not null && _receiptLayoutPath is not null) 151 | { 152 | JsonSerializerOptions options = new JsonSerializerOptions 153 | { 154 | WriteIndented = true, 155 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 156 | }; 157 | 158 | // Write new settings files 159 | await Task.WhenAll( 160 | _settingsCollection.Select(async settingsTuple => 161 | { 162 | settingsTuple.Deconstruct(out var filePath, out var settingsJson); 163 | 164 | var settingsText = settingsJson.ToJsonString(options); 165 | await File.WriteAllTextAsync(filePath, settingsText); 166 | }) 167 | ); 168 | 169 | // Overwrite layout-sets 170 | var layoutSetsText = _layoutSets.ToJsonString(options); 171 | await File.WriteAllTextAsync(Path.Combine(_uiFolder, "layout-sets.json"), layoutSetsText); 172 | 173 | // Move receiptLayout to its own layout-set 174 | Directory.CreateDirectory(Path.Combine(_uiFolder, _receiptLayoutSetName, "layouts")); 175 | File.Move( 176 | _receiptLayoutPath, 177 | Path.Combine(_uiFolder, _receiptLayoutSetName, "layouts", $"{_receiptLayoutName}.json") 178 | ); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | # Non-configurable behaviors 3 | charset = utf-8 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | # Configurable behaviors 8 | # end_of_line = lf - there is no 'auto' with a .editorconfig 9 | indent_style = space 10 | indent_size = 4 11 | max_line_length = 120 12 | 13 | # XML project files 14 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 15 | indent_size = 2 16 | 17 | # XML config files 18 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 19 | indent_size = 2 20 | 21 | [*.{cs,vb}] 22 | dotnet_sort_system_directives_first = true 23 | dotnet_separate_import_directive_groups = false 24 | 25 | end_of_line = crlf 26 | 27 | #### Naming styles #### 28 | 29 | # Interfaces start with 'I' 30 | dotnet_naming_symbols.interface.applicable_kinds = interface 31 | dotnet_naming_style.begins_with_i.required_prefix = I 32 | dotnet_naming_style.begins_with_i.required_suffix = 33 | dotnet_naming_style.begins_with_i.word_separator = 34 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 35 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning 36 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 37 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 38 | 39 | # Types should be PascalCase 40 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, namespace, delegate 41 | dotnet_naming_rule.types_should_be_pascal_case.severity = warning 42 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 43 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 44 | 45 | # Non-private members should be PascalCase 46 | dotnet_naming_symbols.non_private_members.applicable_kinds = property, event, field 47 | dotnet_naming_symbols.non_private_members.applicable_accessibilities = public, internal, protected, protected_internal 48 | dotnet_naming_rule.non_private_members_should_be_pascal_case.severity = warning 49 | dotnet_naming_rule.non_private_members_should_be_pascal_case.symbols = non_private_members 50 | dotnet_naming_rule.non_private_members_should_be_pascal_case.style = pascal_case 51 | 52 | # All methods should be PascalCase 53 | dotnet_naming_symbols.methods.applicable_kinds = method, local_function 54 | dotnet_naming_rule.methods_should_be_pascal_case.severity = warning 55 | dotnet_naming_rule.methods_should_be_pascal_case.symbols = methods 56 | dotnet_naming_rule.methods_should_be_pascal_case.style = pascal_case 57 | 58 | # Private member should be '_' prefixed and camelCase 59 | dotnet_naming_symbols.private_members.applicable_kinds = property, event, field 60 | dotnet_naming_symbols.private_members.applicable_accessibilities = private, private_protected 61 | dotnet_naming_style.__prefixed_camel_case.required_prefix = _ 62 | dotnet_naming_style.__prefixed_camel_case.required_suffix = 63 | dotnet_naming_style.__prefixed_camel_case.word_separator = 64 | dotnet_naming_style.__prefixed_camel_case.capitalization = camel_case 65 | dotnet_naming_rule.private_members_should_be___prefixed_camel_case.severity = warning 66 | dotnet_naming_rule.private_members_should_be___prefixed_camel_case.symbols = private_members 67 | dotnet_naming_rule.private_members_should_be___prefixed_camel_case.style = __prefixed_camel_case 68 | 69 | # Const fields should be PascalCase 70 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning 71 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 72 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case 73 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 74 | dotnet_naming_symbols.constant_fields.required_modifiers = const 75 | 76 | 77 | # General naming styles 78 | 79 | dotnet_naming_style.pascal_case.required_prefix = 80 | dotnet_naming_style.pascal_case.required_suffix = 81 | dotnet_naming_style.pascal_case.word_separator = 82 | dotnet_naming_style.pascal_case.capitalization = pascal_case 83 | 84 | dotnet_style_coalesce_expression = true:suggestion 85 | dotnet_style_null_propagation = true:suggestion 86 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 87 | dotnet_style_prefer_auto_properties = true:silent 88 | dotnet_style_object_initializer = true:suggestion 89 | dotnet_style_collection_initializer = true:suggestion 90 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 91 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 92 | 93 | csharp_using_directive_placement = outside_namespace:silent 94 | csharp_prefer_simple_using_statement = true:suggestion 95 | csharp_prefer_braces = true:silent 96 | csharp_style_prefer_method_group_conversion = true:silent 97 | csharp_style_prefer_top_level_statements = true:silent 98 | csharp_style_expression_bodied_methods = false:silent 99 | csharp_style_expression_bodied_constructors = false:silent 100 | csharp_style_expression_bodied_operators = false:silent 101 | csharp_style_expression_bodied_properties = true:silent 102 | csharp_style_expression_bodied_indexers = true:silent 103 | csharp_style_expression_bodied_accessors = true:silent 104 | csharp_style_expression_bodied_lambdas = true:silent 105 | csharp_style_expression_bodied_local_functions = false:silent 106 | csharp_indent_labels = one_less_than_current 107 | csharp_style_prefer_primary_constructors = false:suggestion 108 | resharper_convert_to_primary_constructor_highlighting = none 109 | csharp_style_namespace_declarations = file_scoped:error 110 | 111 | # Naming rule violation 112 | dotnet_diagnostic.IDE1006.severity = error 113 | 114 | # Unused usings 115 | dotnet_diagnostic.IDE0005.severity = warning 116 | 117 | dotnet_diagnostic.CA1825.severity = warning 118 | 119 | # IDE0052: Remove unread private member 120 | dotnet_diagnostic.IDE0052.severity = warning 121 | 122 | # CA1848: Use the LoggerMessage delegates 123 | dotnet_diagnostic.CA1848.severity = none 124 | 125 | # CA1727: Use PascalCase for named placeholders 126 | dotnet_diagnostic.CA1727.severity = suggestion 127 | 128 | # CA2254: Template should be a static expression 129 | dotnet_diagnostic.CA2254.severity = none 130 | 131 | # CA1822: Mark members as static 132 | dotnet_diagnostic.CA1822.severity = suggestion 133 | 134 | # IDE0080: Remove unnecessary suppression operator 135 | dotnet_diagnostic.IDE0080.severity = error 136 | 137 | # CA1859: Use concrete types when possible for improved performance 138 | dotnet_diagnostic.CA1859.severity = suggestion 139 | 140 | # CA1716: Rename namespace "" so that it no longer conflicts with the reserved language keyword 'Interface' 141 | # TODO: fixing this would be breaking 142 | dotnet_diagnostic.CA1716.severity = suggestion 143 | 144 | # CA1805: Do not initialize unnecessarily 145 | dotnet_diagnostic.CA1805.severity = suggestion 146 | 147 | # CA1711: Identifiers should not have incorrect suffix 148 | # TODO: fixing this would be breaking 149 | dotnet_diagnostic.CA1711.severity = suggestion 150 | 151 | # CA2201: Do not raise reserved exception types 152 | dotnet_diagnostic.CA2201.severity = suggestion 153 | 154 | # CA1720: Identifier contains type name 155 | # TODO: fixing this would be breaking 156 | dotnet_diagnostic.CA1720.severity = suggestion 157 | 158 | # CA1816: Call GC.SuppressFinalize correctly 159 | dotnet_diagnostic.CA1816.severity = warning 160 | 161 | # CA1707: Identifiers should not contain underscores 162 | dotnet_diagnostic.CA1707.severity = none 163 | 164 | # S2325: Methods and properties that don't access instance data should be static 165 | dotnet_diagnostic.S2325.severity = suggestion 166 | 167 | # S3267: Loops should be simplified with "LINQ" expressions 168 | dotnet_diagnostic.S3267.severity = suggestion 169 | 170 | # CS1591: Missing XML comment for publicly visible type or member 171 | dotnet_diagnostic.CS1591.severity = suggestion 172 | 173 | # S125: Sections of code should not be commented out 174 | dotnet_diagnostic.S125.severity = suggestion 175 | 176 | [Program.cs] 177 | dotnet_diagnostic.CA1050.severity = none 178 | dotnet_diagnostic.S1118.severity = none 179 | 180 | [*.{yml,yaml}] 181 | indent_size = 2 182 | end_of_line = lf 183 | 184 | # Verify settings 185 | # https://github.com/VerifyTests/Verify?tab=readme-ov-file#editorconfig-settings 186 | 187 | [*.{received,verified}.{json,txt,xml}] 188 | charset = "utf-8-bom" 189 | end_of_line = lf 190 | indent_size = unset 191 | indent_style = unset 192 | insert_final_newline = false 193 | tab_width = unset 194 | trim_trailing_whitespace = false 195 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutSetRewriter/LayoutSetUpgrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutSetRewriter; 6 | 7 | internal sealed class LayoutSetUpgrader 8 | { 9 | private readonly IList _warnings = new List(); 10 | private readonly string _uiFolder; 11 | private readonly string _layoutSetName; 12 | private readonly string _applicationMetadataFile; 13 | private JsonNode? _layoutSetsJson = null; 14 | 15 | public LayoutSetUpgrader(string uiFolder, string layoutSetName, string applicationMetadataFile) 16 | { 17 | _uiFolder = uiFolder; 18 | _layoutSetName = layoutSetName; 19 | _applicationMetadataFile = applicationMetadataFile; 20 | } 21 | 22 | public IList GetWarnings() 23 | { 24 | return _warnings; 25 | } 26 | 27 | public void Upgrade() 28 | { 29 | // Read applicationmetadata.json file 30 | var appMetaText = File.ReadAllText(_applicationMetadataFile); 31 | var appMetaJson = JsonNode.Parse( 32 | appMetaText, 33 | null, 34 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 35 | ); 36 | if (appMetaJson is not JsonObject appMetaJsonObject) 37 | { 38 | _warnings.Add($"Unable to parse applicationmetadata.json, skipping layout sets upgrade"); 39 | return; 40 | } 41 | 42 | // Read dataTypes array 43 | JsonNode? dataTypes; 44 | try 45 | { 46 | appMetaJsonObject.TryGetPropertyValue("dataTypes", out dataTypes); 47 | } 48 | catch (Exception e) 49 | { 50 | // Duplicate keys in the object will throw an exception here 51 | _warnings.Add( 52 | $"Unable to parse applicationmetadata.json, skipping layout sets upgrade, error: {e.Message}" 53 | ); 54 | return; 55 | } 56 | 57 | if (dataTypes is not JsonArray dataTypesArray) 58 | { 59 | _warnings.Add( 60 | $"dataTypes has unexpected value {dataTypes?.ToJsonString()} in applicationmetadata.json, expected an array" 61 | ); 62 | return; 63 | } 64 | 65 | String? dataTypeId = null; 66 | String? taskId = null; 67 | 68 | foreach (JsonNode? dataType in dataTypesArray) 69 | { 70 | if (dataType is not JsonObject dataTypeObject) 71 | { 72 | _warnings.Add( 73 | $"Unable to parse data type {dataType?.ToJsonString()} in applicationmetadata.json, expected an object" 74 | ); 75 | continue; 76 | } 77 | 78 | if (!dataTypeObject.TryGetPropertyValue("appLogic", out var appLogic)) 79 | { 80 | continue; 81 | } 82 | 83 | if (appLogic is not JsonObject appLogicObject) 84 | { 85 | _warnings.Add( 86 | $"Unable to parse appLogic {appLogic?.ToJsonString()} in applicationmetadata.json, expected an object" 87 | ); 88 | continue; 89 | } 90 | 91 | if (!appLogicObject.ContainsKey("classRef")) 92 | { 93 | continue; 94 | } 95 | 96 | // This object has a class ref, use this datatype and task id 97 | 98 | if (!dataTypeObject.TryGetPropertyValue("id", out var dataTypeIdNode)) 99 | { 100 | _warnings.Add($"Unable to find id in {dataTypeObject.ToJsonString()} in applicationmetadata.json"); 101 | break; 102 | } 103 | 104 | if (!dataTypeObject.TryGetPropertyValue("taskId", out var taskIdNode)) 105 | { 106 | _warnings.Add( 107 | $"Unable to find taskId in dataType {dataTypeIdNode?.ToJsonString()} in applicationmetadata.json" 108 | ); 109 | break; 110 | } 111 | 112 | if ( 113 | dataTypeIdNode is not JsonValue dataTypeIdValue 114 | || dataTypeIdValue.GetValueKind() != JsonValueKind.String 115 | ) 116 | { 117 | _warnings.Add( 118 | $"Unable to parse id {dataTypeIdNode?.ToJsonString()} in applicationmetadata.json, expected a string" 119 | ); 120 | break; 121 | } 122 | 123 | if (taskIdNode is not JsonValue taskIdValue || taskIdValue.GetValueKind() != JsonValueKind.String) 124 | { 125 | _warnings.Add( 126 | $"Unable to parse taskId {taskIdNode?.ToJsonString()} in applicationmetadata.json, expected a string" 127 | ); 128 | break; 129 | } 130 | 131 | dataTypeId = dataTypeIdValue.GetValue(); 132 | taskId = taskIdValue.GetValue(); 133 | break; 134 | } 135 | 136 | if (dataTypeId is null || taskId is null) 137 | { 138 | _warnings.Add( 139 | $"Unable to find a data model (data type with classRef and task) in applicationmetadata.json, skipping layout sets upgrade. Please add a data model and try again." 140 | ); 141 | return; 142 | } 143 | 144 | var layoutSetsJsonString = 145 | $@"{{""$schema"": ""https://altinncdn.no/schemas/json/layout/layout-sets.schema.v1.json"", ""sets"": [{{""id"": ""{_layoutSetName}"", ""dataType"": ""{dataTypeId}"", ""tasks"": [""{taskId}""]}}]}}"; 146 | _layoutSetsJson = JsonNode.Parse(layoutSetsJsonString); 147 | } 148 | 149 | public async Task Write() 150 | { 151 | if (_layoutSetsJson is null) 152 | { 153 | return; 154 | } 155 | 156 | JsonSerializerOptions options = new JsonSerializerOptions 157 | { 158 | WriteIndented = true, 159 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 160 | }; 161 | 162 | // Create new layout set folder 163 | Directory.CreateDirectory(Path.Combine(_uiFolder, _layoutSetName)); 164 | 165 | // Move existing files to new layout set 166 | var oldLayoutsPath = Path.Combine(_uiFolder, "layouts"); 167 | var newLayoutsPath = Path.Combine(_uiFolder, _layoutSetName, "layouts"); 168 | if (Directory.Exists(oldLayoutsPath)) 169 | { 170 | if (Directory.Exists(newLayoutsPath)) 171 | { 172 | if (Directory.EnumerateFileSystemEntries(newLayoutsPath).Any()) 173 | { 174 | throw new InvalidOperationException($"The folder {newLayoutsPath} already exists and is not empty"); 175 | } 176 | 177 | Directory.Delete(newLayoutsPath, false); 178 | } 179 | 180 | Directory.Move(oldLayoutsPath, newLayoutsPath); 181 | } 182 | else 183 | { 184 | Directory.CreateDirectory(newLayoutsPath); 185 | if (File.Exists(Path.Combine(_uiFolder, "FormLayout.json"))) 186 | { 187 | File.Move(Path.Combine(_uiFolder, "FormLayout.json"), Path.Combine(newLayoutsPath, "FormLayout.json")); 188 | } 189 | } 190 | 191 | var oldSettingsPath = Path.Combine(_uiFolder, "Settings.json"); 192 | var newSettingsPath = Path.Combine(_uiFolder, _layoutSetName, "Settings.json"); 193 | if (File.Exists(oldSettingsPath)) 194 | { 195 | File.Move(oldSettingsPath, newSettingsPath); 196 | } 197 | 198 | var oldRuleConfigurationPath = Path.Combine(_uiFolder, "RuleConfiguration.json"); 199 | var newRuleConfigurationPath = Path.Combine(_uiFolder, _layoutSetName, "RuleConfiguration.json"); 200 | if (File.Exists(oldRuleConfigurationPath)) 201 | { 202 | File.Move(oldRuleConfigurationPath, newRuleConfigurationPath); 203 | } 204 | 205 | var oldRuleHandlerPath = Path.Combine(_uiFolder, "RuleHandler.js"); 206 | var newRuleHandlerPath = Path.Combine(_uiFolder, _layoutSetName, "RuleHandler.js"); 207 | if (File.Exists(oldRuleHandlerPath)) 208 | { 209 | File.Move(oldRuleHandlerPath, newRuleHandlerPath); 210 | } 211 | 212 | // Write new layout-sets.json 213 | await File.WriteAllTextAsync( 214 | Path.Combine(_uiFolder, "layout-sets.json"), 215 | _layoutSetsJson.ToJsonString(options) 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/LayoutRewriter/LayoutMutator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter; 6 | 7 | /// 8 | /// Reads all layout files and applies a set of mutators to them before writing them back 9 | /// This class requires that the app has already been converted to using layout sets 10 | /// 11 | internal sealed class LayoutMutator 12 | { 13 | private readonly IList _warnings = new List(); 14 | private readonly Dictionary _layoutCollection = new(); 15 | private readonly string _uiFolder; 16 | 17 | public LayoutMutator(string uiFolder) 18 | { 19 | _uiFolder = uiFolder; 20 | } 21 | 22 | public IList GetWarnings() 23 | { 24 | return _warnings; 25 | } 26 | 27 | public void ReadAllLayoutFiles() 28 | { 29 | var layoutSets = Directory.GetDirectories(_uiFolder); 30 | foreach (var layoutSet in layoutSets) 31 | { 32 | var layoutsFolder = Path.Combine(layoutSet, "layouts"); 33 | if (!Directory.Exists(layoutsFolder)) 34 | { 35 | var compactLayoutsPath = string.Join( 36 | Path.DirectorySeparatorChar, 37 | layoutSet.Split(Path.DirectorySeparatorChar)[^2..] 38 | ); 39 | _warnings.Add($"No layouts folder found in layoutset {compactLayoutsPath}, skipping"); 40 | continue; 41 | } 42 | 43 | var layoutFiles = Directory.GetFiles(layoutsFolder, "*.json"); 44 | foreach (var layoutFile in layoutFiles) 45 | { 46 | var layoutText = File.ReadAllText(layoutFile); 47 | var layoutJson = JsonNode.Parse( 48 | layoutText, 49 | null, 50 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 51 | ); 52 | 53 | if (layoutJson is not JsonObject layoutJsonObject) 54 | { 55 | var compactLayoutFilePath = string.Join( 56 | Path.DirectorySeparatorChar, 57 | layoutFile.Split(Path.DirectorySeparatorChar)[^3..] 58 | ); 59 | _warnings.Add($"Unable to parse {compactLayoutFilePath} as a json object, skipping"); 60 | continue; 61 | } 62 | 63 | _layoutCollection.Add(layoutFile, layoutJsonObject); 64 | } 65 | } 66 | } 67 | 68 | public void Mutate(ILayoutMutator mutator) 69 | { 70 | foreach ((var filePath, var layoutJson) in _layoutCollection) 71 | { 72 | var compactFilePath = string.Join( 73 | Path.DirectorySeparatorChar, 74 | filePath.Split(Path.DirectorySeparatorChar)[^3..] 75 | ); 76 | var components = new List(); 77 | var componentLookup = new Dictionary(); 78 | 79 | layoutJson.TryGetPropertyValue("data", out var dataNode); 80 | if (dataNode is not JsonObject dataObject) 81 | { 82 | _warnings.Add($"Unable to parse data node in {compactFilePath}, expected an object"); 83 | continue; 84 | } 85 | 86 | JsonNode? layoutNode; 87 | try 88 | { 89 | dataObject.TryGetPropertyValue("layout", out layoutNode); 90 | } 91 | catch (Exception e) 92 | { 93 | // Duplicate keys in the object will throw an exception here 94 | _warnings.Add($"Unable to parse layout array in {compactFilePath}, error: {e.Message}"); 95 | continue; 96 | } 97 | 98 | if (layoutNode is not JsonArray layoutArray) 99 | { 100 | _warnings.Add($"Unable to parse layout node in {compactFilePath}, expected an array"); 101 | continue; 102 | } 103 | 104 | foreach (var componentNode in layoutArray) 105 | { 106 | if (componentNode is not JsonObject componentObject) 107 | { 108 | _warnings.Add( 109 | $"Unable to parse component {componentNode?.ToJsonString()} in {compactFilePath}, expected an object" 110 | ); 111 | continue; 112 | } 113 | 114 | JsonNode? idNode; 115 | try 116 | { 117 | componentObject.TryGetPropertyValue("id", out idNode); 118 | } 119 | catch (Exception e) 120 | { 121 | // Duplicate keys in the object will throw an exception here 122 | _warnings.Add( 123 | $"Unable to parse component {componentNode?.ToJsonString()} in {compactFilePath}, error: {e.Message}" 124 | ); 125 | continue; 126 | } 127 | 128 | if (idNode is not JsonValue idValue || idValue.GetValueKind() != JsonValueKind.String) 129 | { 130 | _warnings.Add( 131 | $"Unable to parse id {idNode?.ToJsonString()} in {compactFilePath}, expected a string" 132 | ); 133 | continue; 134 | } 135 | 136 | var id = idValue.GetValue(); 137 | 138 | if (componentLookup.ContainsKey(id)) 139 | { 140 | _warnings.Add($"Duplicate id {id} in {compactFilePath}, skipping upgrade of component"); 141 | continue; 142 | } 143 | 144 | components.Add(componentObject); 145 | componentLookup.Add(id, componentObject); 146 | } 147 | 148 | foreach (var component in components) 149 | { 150 | var result = mutator.Mutate(component.DeepClone().AsObject(), componentLookup); 151 | if (result is SkipResult) 152 | { 153 | continue; 154 | } 155 | 156 | if (result is ErrorResult errorResult) 157 | { 158 | _warnings.Add( 159 | $"Updating component {component["id"]} in {compactFilePath} failed with the message: {errorResult.Message}" 160 | ); 161 | continue; 162 | } 163 | 164 | if (result is DeleteResult deleteResult) 165 | { 166 | if (deleteResult.Warnings.Count > 0) 167 | { 168 | _warnings.Add( 169 | $"Updating component {component["id"]} in {compactFilePath} resulted in the following warnings: {string.Join(", ", deleteResult.Warnings)}" 170 | ); 171 | } 172 | layoutArray.Remove(component); 173 | continue; 174 | } 175 | 176 | if (result is ReplaceResult replaceResult) 177 | { 178 | if (replaceResult.Warnings.Count > 0) 179 | { 180 | _warnings.Add( 181 | $"Updating component {component["id"]} in {compactFilePath} resulted in the following warnings: {string.Join(", ", replaceResult.Warnings)}" 182 | ); 183 | } 184 | var index = layoutArray.IndexOf(component); 185 | layoutArray.RemoveAt(index); 186 | layoutArray.Insert(index, replaceResult.Component); 187 | continue; 188 | } 189 | } 190 | } 191 | } 192 | 193 | public async Task WriteAllLayoutFiles() 194 | { 195 | JsonSerializerOptions options = new JsonSerializerOptions 196 | { 197 | WriteIndented = true, 198 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 199 | }; 200 | 201 | await Task.WhenAll( 202 | _layoutCollection.Select(async layoutTuple => 203 | { 204 | layoutTuple.Deconstruct(out var filePath, out var layoutJson); 205 | 206 | var layoutText = layoutJson.ToJsonString(options); 207 | await File.WriteAllTextAsync(filePath, layoutText); 208 | }) 209 | ); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/ProcessRewriter/ProcessUpgrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Xml; 3 | using System.Xml.Linq; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.ProcessRewriter; 6 | 7 | /// 8 | /// Upgrade the process file 9 | /// 10 | internal sealed class ProcessUpgrader 11 | { 12 | private readonly XDocument _doc; 13 | private readonly string _processFile; 14 | private readonly XNamespace _newAltinnNs = "http://altinn.no/process"; 15 | private readonly XNamespace _origAltinnNs = "http://altinn.no"; 16 | private readonly XNamespace _bpmnNs = "http://www.omg.org/spec/BPMN/20100524/MODEL"; 17 | private readonly IList _warnings = new List(); 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// 23 | public ProcessUpgrader(string processFile) 24 | { 25 | _processFile = processFile; 26 | var xmlString = File.ReadAllText(processFile); 27 | xmlString = xmlString.Replace($"xmlns:altinn=\"{_origAltinnNs}\"", $"xmlns:altinn=\"{_newAltinnNs}\""); 28 | _doc = XDocument.Parse(xmlString); 29 | } 30 | 31 | /// 32 | /// Upgrade the process file, the changes will not be written to disk until Write is called 33 | /// 34 | public void Upgrade() 35 | { 36 | var definitions = _doc.Root; 37 | var process = definitions?.Elements().Single(e => e.Name.LocalName == "process"); 38 | var processElements = process?.Elements() ?? Enumerable.Empty(); 39 | foreach (var processElement in processElements) 40 | { 41 | if (processElement.Name.LocalName == "task") 42 | { 43 | UpgradeTask(processElement); 44 | } 45 | else if (processElement.Name.LocalName == "sequenceFlow") 46 | { 47 | UpgradeSequenceFlow(processElement); 48 | } 49 | } 50 | } 51 | 52 | /// 53 | /// Write the changes to disk 54 | /// 55 | public async Task Write() 56 | { 57 | XmlWriterSettings xws = new XmlWriterSettings(); 58 | xws.Async = true; 59 | xws.OmitXmlDeclaration = false; 60 | xws.Indent = true; 61 | xws.Encoding = Encoding.UTF8; 62 | await using XmlWriter xw = XmlWriter.Create(_processFile, xws); 63 | await _doc.WriteToAsync(xw, CancellationToken.None); 64 | } 65 | 66 | /// 67 | /// Gets the warnings from the upgrade 68 | /// 69 | /// 70 | public IList GetWarnings() 71 | { 72 | return _warnings; 73 | } 74 | 75 | private void UpgradeTask(XElement processElement) 76 | { 77 | var taskTypeAttr = processElement.Attribute(_newAltinnNs + "tasktype"); 78 | var taskType = taskTypeAttr?.Value; 79 | if (taskType is null) 80 | { 81 | return; 82 | } 83 | XElement extensionElements = 84 | processElement.Element(_bpmnNs + "extensionElements") ?? new XElement(_bpmnNs + "extensionElements"); 85 | XElement taskExtensionElement = 86 | extensionElements.Element(_newAltinnNs + "taskExtension") ?? new XElement(_newAltinnNs + "taskExtension"); 87 | XElement taskTypeElement = new XElement(_newAltinnNs + "taskType"); 88 | taskTypeElement.Value = taskType; 89 | taskExtensionElement.Add(taskTypeElement); 90 | extensionElements.Add(taskExtensionElement); 91 | processElement.Add(extensionElements); 92 | taskTypeAttr?.Remove(); 93 | if (taskType.Equals("confirmation", StringComparison.Ordinal)) 94 | { 95 | AddAction(processElement, "confirm"); 96 | } 97 | } 98 | 99 | private void UpgradeSequenceFlow(XElement processElement) 100 | { 101 | var idAttr = processElement.Attribute("id"); 102 | if (idAttr?.Value is null) 103 | throw new InvalidOperationException("SequenceFlow element is missing required 'id' attribute"); 104 | 105 | var sourceRefAttr = processElement.Attribute("sourceRef"); 106 | if (sourceRefAttr?.Value is null) 107 | throw new InvalidOperationException( 108 | $"SequenceFlow '{idAttr.Value}' is missing required 'sourceRef' attribute" 109 | ); 110 | 111 | var targetRefAttr = processElement.Attribute("targetRef"); 112 | if (targetRefAttr?.Value is null) 113 | throw new InvalidOperationException( 114 | $"SequenceFlow '{idAttr.Value}' is missing required 'targetRef' attribute" 115 | ); 116 | 117 | var flowTypeAttr = processElement.Attribute(_newAltinnNs + "flowtype"); 118 | flowTypeAttr?.Remove(); 119 | if (flowTypeAttr?.Value != "AbandonCurrentReturnToNext") 120 | { 121 | return; 122 | } 123 | 124 | SetSequenceFlowAsDefaultIfGateway(sourceRefAttr.Value, idAttr.Value); 125 | var sourceTask = FollowGatewaysAndGetSourceTask(sourceRefAttr.Value); 126 | AddAction(sourceTask, "reject"); 127 | var conditionExpression = processElement 128 | .Elements() 129 | .FirstOrDefault(e => e.Name.LocalName == "conditionExpression"); 130 | if (conditionExpression is null) 131 | { 132 | conditionExpression = new XElement(_bpmnNs + "conditionExpression"); 133 | processElement.Add(conditionExpression); 134 | } 135 | conditionExpression.Value = "[\"equals\", [\"gatewayAction\"],\"reject\"]"; 136 | _warnings.Add( 137 | $"SequenceFlow {idAttr.Value} has flowtype {flowTypeAttr.Value} upgrade tool has tried to add reject action to source task. \nPlease verify that process flow is correct and that layoutfiels are updated to use ActionButtons\nRefere to docs.altinn.studio for how actions in v8 work" 138 | ); 139 | } 140 | 141 | private void SetSequenceFlowAsDefaultIfGateway(string elementRef, string sequenceFlowRef) 142 | { 143 | var sourceElement = _doc 144 | .Root?.Elements() 145 | .Single(e => e.Name.LocalName == "process") 146 | .Elements() 147 | .Single(e => e.Attribute("id")?.Value == elementRef); 148 | if (sourceElement?.Name.LocalName == "exclusiveGateway") 149 | { 150 | if (sourceElement.Attribute("default") is null) 151 | { 152 | sourceElement.Add(new XAttribute("default", sequenceFlowRef)); 153 | } 154 | else 155 | { 156 | _warnings.Add( 157 | $"Default sequence flow already set for gateway {elementRef}. Process is most likely not correct. Please correct it manually and test it." 158 | ); 159 | } 160 | } 161 | } 162 | 163 | private XElement FollowGatewaysAndGetSourceTask(string sourceRef) 164 | { 165 | var processElement = _doc.Root?.Elements().Single(e => e.Name.LocalName == "process"); 166 | var sourceElement = processElement?.Elements().Single(e => e.Attribute("id")?.Value == sourceRef); 167 | if (sourceElement?.Name.LocalName == "task") 168 | { 169 | return sourceElement; 170 | } 171 | 172 | if (sourceElement?.Name.LocalName == "exclusiveGateway") 173 | { 174 | var incomingSequenceFlow = sourceElement.Elements().Single(e => e.Name.LocalName == "incoming").Value; 175 | var incomingSequenceFlowRef = processElement 176 | ?.Elements() 177 | .Single(e => 178 | { 179 | var idAttr = e.Attribute("id"); 180 | if (idAttr?.Value is null) 181 | throw new InvalidOperationException("Process element is missing required 'id' attribute"); 182 | return idAttr.Value == incomingSequenceFlow; 183 | }) 184 | .Attribute("sourceRef") 185 | ?.Value; 186 | if (incomingSequenceFlowRef is null) 187 | throw new InvalidOperationException("Could not find source reference for incoming sequence flow"); 188 | return FollowGatewaysAndGetSourceTask(incomingSequenceFlowRef); 189 | } 190 | 191 | throw new InvalidOperationException("Unexpected element type"); 192 | } 193 | 194 | private void AddAction(XElement sourceTask, string actionName) 195 | { 196 | var extensionElements = sourceTask.Element(_bpmnNs + "extensionElements"); 197 | if (extensionElements is null) 198 | { 199 | extensionElements = new XElement(_bpmnNs + "extensionElements"); 200 | sourceTask.Add(extensionElements); 201 | } 202 | 203 | var taskExtensionElement = extensionElements.Element(_newAltinnNs + "taskExtension"); 204 | if (taskExtensionElement is null) 205 | { 206 | taskExtensionElement = new XElement(_newAltinnNs + "taskExtension"); 207 | extensionElements.Add(taskExtensionElement); 208 | } 209 | 210 | var actions = taskExtensionElement.Element(_newAltinnNs + "actions"); 211 | if (actions is null) 212 | { 213 | actions = new XElement(_newAltinnNs + "actions"); 214 | taskExtensionElement.Add(actions); 215 | } 216 | if (actions.Elements().Any(e => e.Value == actionName)) 217 | { 218 | return; 219 | } 220 | var action = new XElement(_newAltinnNs + "action"); 221 | action.Value = actionName; 222 | actions.Add(action); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/CodeRewriters/TypesRewriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.CodeRewriters; 6 | 7 | /// 8 | /// Rewrite the types of moved interfaces 9 | /// 10 | internal sealed class TypesRewriter : CSharpSyntaxRewriter 11 | { 12 | private readonly SemanticModel _semanticModel; 13 | private readonly Dictionary _fieldDescendantsMapping = new Dictionary() 14 | { 15 | { "Altinn.App.Core.Interface.IAppEvents", SyntaxFactory.IdentifierName("IAppEvents") }, 16 | { "Altinn.App.Core.Interface.IApplication", SyntaxFactory.IdentifierName("IApplicationClient") }, 17 | { "Altinn.App.Core.Interface.IAppResources", SyntaxFactory.IdentifierName("IAppResources") }, 18 | { "Altinn.App.Core.Interface.IAuthentication", SyntaxFactory.IdentifierName("IAuthenticationClient") }, 19 | { "Altinn.App.Core.Interface.IAuthorization", SyntaxFactory.IdentifierName("IAuthorizationClient") }, 20 | { "Altinn.App.Core.Interface.IData", SyntaxFactory.IdentifierName("IDataClient") }, 21 | { "Altinn.App.Core.Interface.IDSF", SyntaxFactory.IdentifierName("IPersonClient") }, 22 | { "Altinn.App.Core.Interface.IER", SyntaxFactory.IdentifierName("IOrganizationClient") }, 23 | { "Altinn.App.Core.Interface.IEvents", SyntaxFactory.IdentifierName("IEventsClient") }, 24 | { "Altinn.App.Core.Interface.IInstance", SyntaxFactory.IdentifierName("IInstanceClient") }, 25 | { "Altinn.App.Core.Interface.IInstanceEvent", SyntaxFactory.IdentifierName("IInstanceEventClient") }, 26 | { "Altinn.App.Core.Interface.IPersonLookup", SyntaxFactory.IdentifierName("IPersonClient") }, 27 | { "Altinn.App.Core.Interface.IPersonRetriever", SyntaxFactory.IdentifierName("IPersonClient") }, 28 | { "Altinn.App.Core.Interface.IPrefill", SyntaxFactory.IdentifierName("IPrefill") }, 29 | { "Altinn.App.Core.Interface.IProcess", SyntaxFactory.IdentifierName("IProcessClient") }, 30 | { "Altinn.App.Core.Interface.IProfile", SyntaxFactory.IdentifierName("IProfileClient") }, 31 | { "Altinn.App.Core.Interface.IRegister", SyntaxFactory.IdentifierName("IAltinnPartyClient") }, 32 | { "Altinn.App.Core.Interface.ISecrets", SyntaxFactory.IdentifierName("ISecretsClient") }, 33 | { "Altinn.App.Core.Interface.ITaskEvents", SyntaxFactory.IdentifierName("ITaskEvents") }, 34 | { "Altinn.App.Core.Interface.IUserTokenProvider", SyntaxFactory.IdentifierName("IUserTokenProvider") }, 35 | }; 36 | private readonly IEnumerable _statementsToRemove = new List() 37 | { 38 | "app.UseDefaultSecurityHeaders();", 39 | "app.UseRouting();", 40 | "app.UseStaticFiles('/' + applicationId);", 41 | "app.UseAuthentication();", 42 | "app.UseAuthorization();", 43 | "app.UseEndpoints(endpoints", 44 | "app.UseHealthChecks(\"/health\");", 45 | "app.UseAltinnAppCommonConfiguration();", 46 | }; 47 | 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// 52 | public TypesRewriter(SemanticModel semanticModel) 53 | { 54 | _semanticModel = semanticModel; 55 | } 56 | 57 | /// 58 | public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) 59 | { 60 | return UpdateField(node); 61 | } 62 | 63 | /// 64 | public override SyntaxNode VisitParameter(ParameterSyntax node) 65 | { 66 | var parameterTypeName = node.Type; 67 | if (parameterTypeName is null) 68 | { 69 | return node; 70 | } 71 | var parameterType = (ITypeSymbol?)_semanticModel.GetSymbolInfo(parameterTypeName).Symbol; 72 | var parameterTypeString = parameterType?.ToString(); 73 | if ( 74 | parameterTypeString is not null 75 | && _fieldDescendantsMapping.TryGetValue(parameterTypeString, out var newType) 76 | ) 77 | { 78 | var newTypeName = newType 79 | .WithLeadingTrivia(parameterTypeName.GetLeadingTrivia()) 80 | .WithTrailingTrivia(parameterTypeName.GetTrailingTrivia()); 81 | return node.ReplaceNode(parameterTypeName, newTypeName); 82 | } 83 | 84 | return node; 85 | } 86 | 87 | /// 88 | public override SyntaxNode VisitGlobalStatement(GlobalStatementSyntax node) 89 | { 90 | if ( 91 | node.Statement is LocalFunctionStatementSyntax localFunctionStatementSyntax 92 | && localFunctionStatementSyntax.Identifier.Text == "Configure" 93 | && !localFunctionStatementSyntax.ParameterList.Parameters.Any() 94 | && localFunctionStatementSyntax.Body is not null 95 | ) 96 | { 97 | SyntaxTriviaList leadingTrivia = SyntaxFactory.TriviaList(); 98 | SyntaxTriviaList trailingTrivia = SyntaxFactory.TriviaList(); 99 | var newBody = SyntaxFactory 100 | .Block() 101 | .WithoutLeadingTrivia() 102 | .WithTrailingTrivia(localFunctionStatementSyntax.Body.GetTrailingTrivia()); 103 | foreach (var childNode in localFunctionStatementSyntax.Body.ChildNodes()) 104 | { 105 | if ( 106 | childNode is IfStatementSyntax ifStatementSyntax 107 | && ifStatementSyntax.Condition.ToString() != "app.Environment.IsDevelopment()" 108 | ) 109 | { 110 | newBody = AddStatementWithTrivia(newBody, ifStatementSyntax); 111 | } 112 | if (childNode is ExpressionStatementSyntax statementSyntax) 113 | { 114 | leadingTrivia = statementSyntax.GetLeadingTrivia(); 115 | trailingTrivia = statementSyntax.GetTrailingTrivia(); 116 | if (!ShouldRemoveStatement(statementSyntax)) 117 | { 118 | newBody = AddStatementWithTrivia(newBody, statementSyntax); 119 | } 120 | } 121 | if (childNode is LocalDeclarationStatementSyntax localDeclarationStatement) 122 | { 123 | newBody = AddStatementWithTrivia(newBody, localDeclarationStatement); 124 | } 125 | } 126 | newBody = newBody.AddStatements( 127 | SyntaxFactory 128 | .ParseStatement("app.UseAltinnAppCommonConfiguration();") 129 | .WithLeadingTrivia(leadingTrivia) 130 | .WithTrailingTrivia(trailingTrivia) 131 | ); 132 | return node.ReplaceNode(localFunctionStatementSyntax.Body, newBody); 133 | } 134 | 135 | return node; 136 | } 137 | 138 | /// 139 | public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) 140 | { 141 | if ( 142 | node.Identifier.Text == "FilterAsync" 143 | && node.Parent is ClassDeclarationSyntax { BaseList: not null } classDeclarationSyntax 144 | && classDeclarationSyntax.BaseList.Types.Any(x => x.Type.ToString() == "IProcessExclusiveGateway") 145 | && node.ParameterList.Parameters.All(x => x.Type?.ToString() != "ProcessGatewayInformation") 146 | ) 147 | { 148 | return node.AddParameterListParameters( 149 | SyntaxFactory 150 | .Parameter( 151 | SyntaxFactory 152 | .Identifier("processGatewayInformation") 153 | .WithLeadingTrivia(SyntaxFactory.ElasticSpace) 154 | ) 155 | .WithType( 156 | SyntaxFactory 157 | .ParseTypeName("ProcessGatewayInformation") 158 | .WithLeadingTrivia(SyntaxFactory.ElasticSpace) 159 | ) 160 | ); 161 | } 162 | 163 | return node; 164 | } 165 | 166 | private FieldDeclarationSyntax UpdateField(FieldDeclarationSyntax node) 167 | { 168 | var variableTypeName = node.Declaration.Type; 169 | var variableType = (ITypeSymbol?)_semanticModel.GetSymbolInfo(variableTypeName).Symbol; 170 | var variableTypeString = variableType?.ToString(); 171 | if (variableTypeString is not null && _fieldDescendantsMapping.TryGetValue(variableTypeString, out var newType)) 172 | { 173 | var newTypeName = newType 174 | .WithLeadingTrivia(variableTypeName.GetLeadingTrivia()) 175 | .WithTrailingTrivia(variableTypeName.GetTrailingTrivia()); 176 | node = node.ReplaceNode(variableTypeName, newTypeName); 177 | Console.WriteLine( 178 | $"Updated field {node.Declaration.Variables.First().Identifier.Text} from {variableType} to {newType}" 179 | ); 180 | } 181 | return node; 182 | } 183 | 184 | private bool ShouldRemoveStatement(StatementSyntax statementSyntax) 185 | { 186 | foreach (var statementToRemove in _statementsToRemove) 187 | { 188 | var s = statementSyntax.ToString(); 189 | if (s == statementToRemove || s.StartsWith(statementToRemove, StringComparison.Ordinal)) 190 | { 191 | return true; 192 | } 193 | } 194 | return false; 195 | } 196 | 197 | private static BlockSyntax AddStatementWithTrivia(BlockSyntax block, StatementSyntax statement) 198 | { 199 | return block 200 | .AddStatements(statement) 201 | .WithLeadingTrivia(statement.GetLeadingTrivia()) 202 | .WithTrailingTrivia(statement.GetTrailingTrivia()); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/SchemaRefRewriter/SchemaRefUpgrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.SchemaRefRewriter; 6 | 7 | /// 8 | /// Upgrades schema refs 9 | /// Assumes that layout set conversion has already been done 10 | /// 11 | internal sealed class SchemaRefUpgrader 12 | { 13 | private readonly IList _warnings = new List(); 14 | private readonly Dictionary _files = new(); 15 | private readonly string _uiFolder; 16 | private readonly string _applicationMetadataFile; 17 | private readonly string _textsFolder; 18 | private readonly string _layoutSchemaUri; 19 | private readonly string _layoutSetsSchemaUri; 20 | private readonly string _layoutSettingsSchemaUri; 21 | private readonly string _footerSchemaUri; 22 | private readonly string _applicationMetadataSchemaUri; 23 | private readonly string _textResourcesSchemaUri; 24 | 25 | public SchemaRefUpgrader(string targetVersion, string uiFolder, string applicationMetadataFile, string textsFolder) 26 | { 27 | _uiFolder = uiFolder; 28 | _applicationMetadataFile = applicationMetadataFile; 29 | _textsFolder = textsFolder; 30 | 31 | _layoutSchemaUri = 32 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/layout/layout.schema.v1.json"; 33 | _layoutSetsSchemaUri = 34 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/layout/layout-sets.schema.v1.json"; 35 | _layoutSettingsSchemaUri = 36 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/layout/layoutSettings.schema.v1.json"; 37 | _footerSchemaUri = 38 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/layout/footer.schema.v1.json"; 39 | _applicationMetadataSchemaUri = 40 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/application/application-metadata.schema.v1.json"; 41 | _textResourcesSchemaUri = 42 | $"https://altinncdn.no/toolkits/altinn-app-frontend/{targetVersion}/schemas/json/text-resources/text-resources.schema.v1.json"; 43 | } 44 | 45 | public IList GetWarnings() 46 | { 47 | return _warnings; 48 | } 49 | 50 | public void Upgrade() 51 | { 52 | // Application metadata 53 | var appMetaJson = JsonNode.Parse( 54 | File.ReadAllText(_applicationMetadataFile), 55 | null, 56 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 57 | ); 58 | if (appMetaJson is JsonObject appMetaJsonObject) 59 | { 60 | _files.Add(_applicationMetadataFile, WithSchemaRef(appMetaJsonObject, _applicationMetadataSchemaUri)); 61 | } 62 | else 63 | { 64 | _warnings.Add("Unable to parse applicationmetadata.json, skipping schema ref upgrade"); 65 | } 66 | 67 | // Text resources 68 | var textResourceFiles = Directory.GetFiles(_textsFolder, "*.json"); 69 | foreach (var textResourceFile in textResourceFiles) 70 | { 71 | var textResourceJson = JsonNode.Parse( 72 | File.ReadAllText(textResourceFile), 73 | null, 74 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 75 | ); 76 | if (textResourceJson is JsonObject textResourceJsonObject) 77 | { 78 | _files.Add(textResourceFile, WithSchemaRef(textResourceJsonObject, _textResourcesSchemaUri)); 79 | } 80 | else 81 | { 82 | var compactFilePath = string.Join( 83 | Path.DirectorySeparatorChar, 84 | textResourceFile.Split(Path.DirectorySeparatorChar)[^2..] 85 | ); 86 | _warnings.Add($"Unable to parse {compactFilePath}, skipping schema ref upgrade"); 87 | } 88 | } 89 | 90 | // Footer 91 | var footerFile = Path.Combine(_uiFolder, "footer.json"); 92 | if (File.Exists(footerFile)) 93 | { 94 | var footerJson = JsonNode.Parse( 95 | File.ReadAllText(footerFile), 96 | null, 97 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 98 | ); 99 | if (footerJson is JsonObject footerJsonObject) 100 | { 101 | _files.Add(footerFile, WithSchemaRef(footerJsonObject, _footerSchemaUri)); 102 | } 103 | else 104 | { 105 | _warnings.Add("Unable to parse footer.json, skipping schema ref upgrade"); 106 | } 107 | } 108 | 109 | // Layout sets 110 | var layoutSetsFile = Path.Combine(_uiFolder, "layout-sets.json"); 111 | var layoutSetsJson = JsonNode.Parse( 112 | File.ReadAllText(layoutSetsFile), 113 | null, 114 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 115 | ); 116 | if (layoutSetsJson is JsonObject layoutSetsJsonObject) 117 | { 118 | _files.Add(layoutSetsFile, WithSchemaRef(layoutSetsJsonObject, _layoutSetsSchemaUri)); 119 | } 120 | else 121 | { 122 | _warnings.Add("Unable to parse layout-sets.json, skipping schema ref upgrade"); 123 | } 124 | 125 | // Layouts and layout settings 126 | var layoutSets = Directory.GetDirectories(_uiFolder); 127 | foreach (var layoutSet in layoutSets) 128 | { 129 | // Layout settings 130 | var layoutSettingsFile = Path.Combine(layoutSet, "Settings.json"); 131 | var compactSettingsFilePath = string.Join( 132 | Path.DirectorySeparatorChar, 133 | layoutSettingsFile.Split(Path.DirectorySeparatorChar)[^2..] 134 | ); 135 | if (File.Exists(layoutSettingsFile)) 136 | { 137 | var layoutSettingsJson = JsonNode.Parse( 138 | File.ReadAllText(layoutSettingsFile), 139 | null, 140 | new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true } 141 | ); 142 | if (layoutSettingsJson is JsonObject layoutSettingsJsonObject) 143 | { 144 | _files.Add(layoutSettingsFile, WithSchemaRef(layoutSettingsJsonObject, _layoutSettingsSchemaUri)); 145 | } 146 | else 147 | { 148 | _warnings.Add($"Unable to parse {compactSettingsFilePath}, skipping schema ref upgrade"); 149 | } 150 | } 151 | else 152 | { 153 | _warnings.Add($"Could not find {compactSettingsFilePath}, skipping schema ref upgrade"); 154 | } 155 | 156 | // Layout files 157 | var layoutsFolder = Path.Combine(layoutSet, "layouts"); 158 | if (Directory.Exists(layoutsFolder)) 159 | { 160 | var layoutFiles = Directory.GetFiles(layoutsFolder, "*.json"); 161 | foreach (var layoutFile in layoutFiles) 162 | { 163 | var layoutJson = JsonNode.Parse( 164 | File.ReadAllText(layoutFile), 165 | null, 166 | new JsonDocumentOptions() 167 | { 168 | CommentHandling = JsonCommentHandling.Skip, 169 | AllowTrailingCommas = true, 170 | } 171 | ); 172 | if (layoutJson is JsonObject layoutJsonObject) 173 | { 174 | _files.Add(layoutFile, WithSchemaRef(layoutJsonObject, _layoutSchemaUri)); 175 | } 176 | else 177 | { 178 | var compactLayoutFilePath = string.Join( 179 | Path.DirectorySeparatorChar, 180 | layoutFile.Split(Path.DirectorySeparatorChar)[^3..] 181 | ); 182 | _warnings.Add($"Unable to parse {compactLayoutFilePath}, skipping schema ref upgrade"); 183 | } 184 | } 185 | } 186 | else 187 | { 188 | var compactLayoutsPath = string.Join( 189 | Path.DirectorySeparatorChar, 190 | layoutSet.Split(Path.DirectorySeparatorChar)[^2..] 191 | ); 192 | _warnings.Add($"No layouts folder found in layoutset {compactLayoutsPath}, skipping"); 193 | } 194 | } 195 | } 196 | 197 | public JsonObject WithSchemaRef(JsonObject json, string schemaUrl) 198 | { 199 | json.Remove("$schema"); 200 | 201 | var schemaProperty = new KeyValuePair("$schema", JsonValue.Create(schemaUrl)); 202 | 203 | return new JsonObject( 204 | json.AsEnumerable() 205 | .Select(n => KeyValuePair.Create(n.Key, n.Value?.DeepClone())) 206 | .Prepend(schemaProperty) 207 | ); 208 | } 209 | 210 | public async Task Write() 211 | { 212 | JsonSerializerOptions options = new JsonSerializerOptions 213 | { 214 | WriteIndented = true, 215 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 216 | }; 217 | 218 | await Task.WhenAll( 219 | _files.Select(async fileTuple => 220 | { 221 | fileTuple.Deconstruct(out var filePath, out var json); 222 | 223 | var layoutText = json.ToJsonString(options); 224 | await File.WriteAllTextAsync(filePath, layoutText); 225 | }) 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.CodeRewriters; 6 | 7 | /// 8 | /// Rewrites the IDataProcessor implementations 9 | /// 10 | internal sealed class DataProcessorRewriter : CSharpSyntaxRewriter 11 | { 12 | /// 13 | public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) 14 | { 15 | // Ignore any classes that don't implement `IDataProcessor` (consider using semantic model to ensure correct reference) 16 | if (node.BaseList?.Types.Any(t => t.Type.ToString() == "IDataProcessor") == true) 17 | { 18 | var processDataWrite = node 19 | .Members.OfType() 20 | .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataWrite"); 21 | if (processDataWrite is not null) 22 | { 23 | node = node.ReplaceNode(processDataWrite, Update_DataProcessWrite(processDataWrite)); 24 | } 25 | 26 | var processDataRead = node 27 | .Members.OfType() 28 | .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataRead"); 29 | if (processDataRead is not null) 30 | { 31 | node = node.ReplaceNode(processDataRead, Update_DataProcessRead(processDataRead)); 32 | } 33 | } 34 | 35 | return base.VisitClassDeclaration(node); 36 | } 37 | 38 | private MethodDeclarationSyntax Update_DataProcessRead(MethodDeclarationSyntax processDataRead) 39 | { 40 | if ( 41 | processDataRead.ParameterList.Parameters.Count == 3 42 | && processDataRead.ReturnType.ToString() == "Task" 43 | ) 44 | { 45 | processDataRead = AddParameterToProcessDataRead(processDataRead); 46 | processDataRead = ChangeReturnType_FromTaskBool_ToTask(processDataRead); 47 | } 48 | 49 | return processDataRead; 50 | } 51 | 52 | private MethodDeclarationSyntax Update_DataProcessWrite(MethodDeclarationSyntax processDataWrite) 53 | { 54 | if ( 55 | processDataWrite.ParameterList.Parameters.Count == 3 56 | && processDataWrite.ReturnType.ToString() == "Task" 57 | ) 58 | { 59 | processDataWrite = AddParameterToProcessDataWrite(processDataWrite); 60 | processDataWrite = ChangeReturnType_FromTaskBool_ToTask(processDataWrite); 61 | } 62 | 63 | return processDataWrite; 64 | } 65 | 66 | private MethodDeclarationSyntax AddParameterToProcessDataWrite(MethodDeclarationSyntax method) 67 | { 68 | return method.ReplaceNode( 69 | method.ParameterList, 70 | method.ParameterList.AddParameters( 71 | SyntaxFactory 72 | .Parameter(SyntaxFactory.Identifier("previousData")) 73 | .WithLeadingTrivia(SyntaxFactory.Space) 74 | .WithType(SyntaxFactory.ParseTypeName("object?")) 75 | .WithLeadingTrivia(SyntaxFactory.Space), 76 | SyntaxFactory 77 | .Parameter(SyntaxFactory.Identifier("language")) 78 | .WithLeadingTrivia(SyntaxFactory.Space) 79 | .WithType(SyntaxFactory.ParseTypeName("string?")) 80 | .WithLeadingTrivia(SyntaxFactory.Space) 81 | ) 82 | ); 83 | } 84 | 85 | private MethodDeclarationSyntax AddParameterToProcessDataRead(MethodDeclarationSyntax method) 86 | { 87 | return method.ReplaceNode( 88 | method.ParameterList, 89 | method.ParameterList.AddParameters( 90 | SyntaxFactory 91 | .Parameter(SyntaxFactory.Identifier("language")) 92 | .WithLeadingTrivia(SyntaxFactory.Space) 93 | .WithType(SyntaxFactory.ParseTypeName("string?")) 94 | .WithLeadingTrivia(SyntaxFactory.Space) 95 | ) 96 | ); 97 | } 98 | 99 | private MethodDeclarationSyntax ChangeReturnType_FromTaskBool_ToTask(MethodDeclarationSyntax method) 100 | { 101 | if (method.ReturnType.ToString() == "Task") 102 | { 103 | var returnTypeRewriter = new ReturnTypeTaskBooleanRewriter(); 104 | method = (MethodDeclarationSyntax)returnTypeRewriter.Visit(method); 105 | } 106 | 107 | return method; 108 | } 109 | } 110 | 111 | /// 112 | /// Rewrites the return type of a method from `Task` to `Task` 113 | /// 114 | internal sealed class ReturnTypeTaskBooleanRewriter : CSharpSyntaxRewriter 115 | { 116 | /// 117 | public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) 118 | { 119 | if (node.ReturnType.ToString() == "Task") 120 | { 121 | // Change return type 122 | node = node.WithReturnType(SyntaxFactory.ParseTypeName("Task").WithTrailingTrivia(SyntaxFactory.Space)); 123 | } 124 | return base.VisitMethodDeclaration(node); 125 | } 126 | 127 | /// 128 | public override SyntaxNode? VisitBlock(BlockSyntax node) 129 | { 130 | foreach (var returnStatementSyntax in node.Statements.OfType()) 131 | { 132 | var leadingTrivia = returnStatementSyntax.GetLeadingTrivia(); 133 | var trailingTrivia = returnStatementSyntax.GetTrailingTrivia(); 134 | // When we add multiple lines of code, we need the indentation and a newline 135 | var leadingTriviaMiddle = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); 136 | var trailingTriviaMiddle = trailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); 137 | // If we don't find a newline, just guess that LF is used. Will likely work anyway. 138 | if (trailingTriviaMiddle == default) 139 | trailingTriviaMiddle = SyntaxFactory.LineFeed; 140 | 141 | switch (returnStatementSyntax.Expression) 142 | { 143 | // return true/false/variableName 144 | case IdentifierNameSyntax: 145 | case LiteralExpressionSyntax: 146 | case null: 147 | node = node.ReplaceNode( 148 | returnStatementSyntax, 149 | SyntaxFactory 150 | .ReturnStatement() 151 | .WithLeadingTrivia(leadingTrivia) 152 | .WithTrailingTrivia(trailingTrivia) 153 | ); 154 | break; 155 | // case "Task.FromResult(...)": 156 | case InvocationExpressionSyntax 157 | { 158 | Expression: MemberAccessExpressionSyntax 159 | { 160 | Expression: IdentifierNameSyntax { Identifier: { Text: "Task" } }, 161 | Name: { Identifier: { Text: "FromResult" } } 162 | }, 163 | ArgumentList: { Arguments: { Count: 1 } } 164 | }: 165 | node = node.ReplaceNode( 166 | returnStatementSyntax, 167 | SyntaxFactory 168 | .ReturnStatement(SyntaxFactory.ParseExpression(" Task.CompletedTask")) 169 | .WithLeadingTrivia(leadingTrivia) 170 | .WithTrailingTrivia(trailingTrivia) 171 | ); 172 | break; 173 | // case "await Task.FromResult(...)": 174 | // Assume we need an await to silence CS1998 and rewrite to 175 | // await Task.CompletedTask; return; 176 | // Could be dropped if we ignore CS1998 177 | case AwaitExpressionSyntax 178 | { 179 | Expression: InvocationExpressionSyntax 180 | { 181 | Expression: MemberAccessExpressionSyntax 182 | { 183 | Expression: IdentifierNameSyntax { Identifier: { Text: "Task" } }, 184 | Name: { Identifier: { Text: "FromResult" } } 185 | }, 186 | ArgumentList: { Arguments: [{ Expression: IdentifierNameSyntax or LiteralExpressionSyntax }] } 187 | } 188 | }: 189 | node = node.WithStatements( 190 | node.Statements.ReplaceRange( 191 | returnStatementSyntax, 192 | new StatementSyntax[] 193 | { 194 | // Uncomment if cs1998 isn't disabled 195 | // SyntaxFactory.ParseStatement("await Task.CompletedTask;") 196 | // .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle), 197 | 198 | SyntaxFactory 199 | .ReturnStatement() 200 | .WithLeadingTrivia(leadingTriviaMiddle) 201 | .WithTrailingTrivia(trailingTrivia), 202 | } 203 | ) 204 | ); 205 | break; 206 | // Just add move the return; statement after the existing return value 207 | default: 208 | node = node.WithStatements( 209 | node.Statements.ReplaceRange( 210 | returnStatementSyntax, 211 | new StatementSyntax[] 212 | { 213 | SyntaxFactory 214 | .ExpressionStatement(returnStatementSyntax.Expression) 215 | .WithLeadingTrivia(leadingTrivia) 216 | .WithTrailingTrivia(trailingTriviaMiddle), 217 | SyntaxFactory 218 | .ReturnStatement() 219 | .WithLeadingTrivia(leadingTriviaMiddle) 220 | .WithTrailingTrivia(trailingTrivia), 221 | } 222 | ) 223 | ); 224 | break; 225 | } 226 | } 227 | 228 | return base.VisitBlock(node); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Backend/v7Tov8/BackendUpgrade/BackendUpgrade.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.CodeRewriters; 4 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.DockerfileRewriters; 5 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.ProcessRewriter; 6 | using Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.ProjectRewriters; 7 | using Microsoft.Build.Locator; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using Microsoft.CodeAnalysis.MSBuild; 11 | 12 | namespace Altinn.Studio.Cli.Upgrade.Backend.v7Tov8.BackendUpgrade; 13 | 14 | /// 15 | /// Defines the upgrade command for upgrading app-lib-dotnet in an Altinn 3 application 16 | /// 17 | internal static class BackendUpgrade 18 | { 19 | /// 20 | /// Get the actual upgrade command 21 | /// 22 | /// Option for setting the root folder of the project 23 | /// 24 | public static Command GetUpgradeCommand(Option projectFolderOption) 25 | { 26 | var projectFileOption = new Option( 27 | name: "--project", 28 | description: "The project file to read relative to --folder", 29 | getDefaultValue: () => "App/App.csproj" 30 | ); 31 | var processFileOption = new Option( 32 | name: "--process", 33 | description: "The process file to read relative to --folder", 34 | getDefaultValue: () => "App/config/process/process.bpmn" 35 | ); 36 | var appSettingsFolderOption = new Option( 37 | name: "--appsettings-folder", 38 | description: "The folder where the appsettings.*.json files are located", 39 | getDefaultValue: () => "App" 40 | ); 41 | var targetVersionOption = new Option( 42 | name: "--target-version", 43 | description: "The target version to upgrade to", 44 | getDefaultValue: () => "8.7.0" 45 | ); 46 | var targetFrameworkOption = new Option( 47 | name: "--target-framework", 48 | description: "The target dotnet framework version to upgrade to", 49 | getDefaultValue: () => "net8.0" 50 | ); 51 | var skipCsprojUpgradeOption = new Option( 52 | name: "--skip-csproj-upgrade", 53 | description: "Skip csproj file upgrade", 54 | getDefaultValue: () => false 55 | ); 56 | var skipDockerUpgradeOption = new Option( 57 | name: "--skip-dockerfile-upgrade", 58 | description: "Skip Dockerfile upgrade", 59 | getDefaultValue: () => false 60 | ); 61 | var skipCodeUpgradeOption = new Option( 62 | name: "--skip-code-upgrade", 63 | description: "Skip code upgrade", 64 | getDefaultValue: () => false 65 | ); 66 | var skipProcessUpgradeOption = new Option( 67 | name: "--skip-process-upgrade", 68 | description: "Skip process file upgrade", 69 | getDefaultValue: () => false 70 | ); 71 | var skipAppSettingsUpgradeOption = new Option( 72 | name: "--skip-appsettings-upgrade", 73 | description: "Skip appsettings file upgrade", 74 | getDefaultValue: () => false 75 | ); 76 | var upgradeCommand = new Command("backend", "Upgrade an app from app-lib-dotnet v7 to v8") 77 | { 78 | projectFolderOption, 79 | projectFileOption, 80 | processFileOption, 81 | appSettingsFolderOption, 82 | targetVersionOption, 83 | targetFrameworkOption, 84 | skipCsprojUpgradeOption, 85 | skipDockerUpgradeOption, 86 | skipCodeUpgradeOption, 87 | skipProcessUpgradeOption, 88 | skipAppSettingsUpgradeOption, 89 | }; 90 | int returnCode = 0; 91 | upgradeCommand.SetHandler(async context => 92 | { 93 | var projectFolder = context.ParseResult.GetValueForOption(projectFolderOption); 94 | var projectFile = context.ParseResult.GetValueForOption(projectFileOption); 95 | var processFile = context.ParseResult.GetValueForOption(processFileOption); 96 | var appSettingsFolder = context.ParseResult.GetValueForOption(appSettingsFolderOption); 97 | var targetVersion = context.ParseResult.GetValueForOption(targetVersionOption); 98 | var targetFramework = context.ParseResult.GetValueForOption(targetFrameworkOption); 99 | var skipCodeUpgrade = context.ParseResult.GetValueForOption(skipCodeUpgradeOption); 100 | var skipProcessUpgrade = context.ParseResult.GetValueForOption(skipProcessUpgradeOption); 101 | var skipCsprojUpgrade = context.ParseResult.GetValueForOption(skipCsprojUpgradeOption); 102 | var skipDockerUpgrade = context.ParseResult.GetValueForOption(skipDockerUpgradeOption); 103 | var skipAppSettingsUpgrade = context.ParseResult.GetValueForOption(skipAppSettingsUpgradeOption); 104 | 105 | if (projectFolder is null or "CurrentDirectory") 106 | projectFolder = Directory.GetCurrentDirectory(); 107 | 108 | if (projectFile is null) 109 | ExitWithError("Project file option is required but was not provided"); 110 | 111 | if (processFile is null) 112 | ExitWithError("Process file option is required but was not provided"); 113 | 114 | if (appSettingsFolder is null) 115 | ExitWithError("App settings folder option is required but was not provided"); 116 | 117 | if (targetVersion is null) 118 | ExitWithError("Target version option is required but was not provided"); 119 | 120 | if (targetFramework is null) 121 | ExitWithError("Target framework option is required but was not provided"); 122 | 123 | if (!Directory.Exists(projectFolder)) 124 | { 125 | ExitWithError( 126 | $"{projectFolder} does not exist. Please supply location of project with --folder [path/to/project]" 127 | ); 128 | } 129 | 130 | FileAttributes attr = File.GetAttributes(projectFolder); 131 | if ((attr & FileAttributes.Directory) != FileAttributes.Directory) 132 | { 133 | ExitWithError( 134 | $"Project folder {projectFolder} is a file. Please supply location of project with --folder [path/to/project]" 135 | ); 136 | } 137 | 138 | if (!Path.IsPathRooted(projectFolder)) 139 | { 140 | projectFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, projectFile); 141 | processFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, processFile); 142 | appSettingsFolder = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, appSettingsFolder); 143 | } 144 | else 145 | { 146 | projectFile = Path.Combine(projectFolder, projectFile); 147 | processFile = Path.Combine(projectFolder, processFile); 148 | appSettingsFolder = Path.Combine(projectFolder, appSettingsFolder); 149 | } 150 | 151 | if (!File.Exists(projectFile)) 152 | { 153 | ExitWithError( 154 | $"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]" 155 | ); 156 | } 157 | 158 | var projectChecks = new ProjectChecks.ProjectChecks(projectFile); 159 | if (!projectChecks.SupportedSourceVersion()) 160 | { 161 | ExitWithError( 162 | $"Version(s) in project file {projectFile} is not supported. Please upgrade to version 7.0.0 or higher.", 163 | exitCode: 2 164 | ); 165 | } 166 | 167 | if (!skipCodeUpgrade) 168 | { 169 | returnCode = await UpgradeCode(projectFile); 170 | } 171 | 172 | if (!skipCsprojUpgrade && returnCode == 0) 173 | { 174 | returnCode = await UpgradeProjectFile(projectFile, targetVersion, targetFramework); 175 | } 176 | 177 | if (!skipDockerUpgrade && returnCode == 0) 178 | { 179 | returnCode = await UpgradeDockerfile(Path.Combine(projectFolder, "Dockerfile"), targetFramework); 180 | } 181 | 182 | if (!skipProcessUpgrade && returnCode == 0) 183 | { 184 | returnCode = await UpgradeProcess(processFile); 185 | } 186 | 187 | if (!skipAppSettingsUpgrade && returnCode == 0) 188 | { 189 | returnCode = await UpgradeAppSettings(appSettingsFolder); 190 | } 191 | 192 | if (returnCode == 0) 193 | { 194 | Console.WriteLine( 195 | "Upgrade completed without errors. Please verify that the application is still working as expected." 196 | ); 197 | } 198 | else 199 | { 200 | Console.WriteLine("Upgrade completed with errors. Please check for errors in the log above."); 201 | } 202 | Environment.Exit(returnCode); 203 | }); 204 | 205 | return upgradeCommand; 206 | } 207 | 208 | static async Task UpgradeProjectFile(string projectFile, string targetVersion, string targetFramework) 209 | { 210 | await Console.Out.WriteLineAsync("Trying to upgrade nuget versions in project file"); 211 | var rewriter = new ProjectFileRewriter(projectFile, targetVersion, targetFramework); 212 | await rewriter.Upgrade(); 213 | await Console.Out.WriteLineAsync("Nuget versions upgraded"); 214 | return 0; 215 | } 216 | 217 | static async Task UpgradeDockerfile(string dockerFile, string targetFramework) 218 | { 219 | if (!File.Exists(dockerFile)) 220 | { 221 | await Console.Error.WriteLineAsync( 222 | $"Dockerfile {dockerFile} does not exist. Please supply location of project with --dockerfile [path/to/Dockerfile]" 223 | ); 224 | return 1; 225 | } 226 | await Console.Out.WriteLineAsync("Trying to upgrade dockerfile"); 227 | var rewriter = new DockerfileRewriter(dockerFile, targetFramework); 228 | await rewriter.Upgrade(); 229 | await Console.Out.WriteLineAsync("Dockerfile upgraded"); 230 | return 0; 231 | } 232 | 233 | static async Task UpgradeCode(string projectFile) 234 | { 235 | await Console.Out.WriteLineAsync("Trying to upgrade references and using in code"); 236 | 237 | MSBuildLocator.RegisterDefaults(); 238 | var workspace = MSBuildWorkspace.Create(); 239 | var project = await workspace.OpenProjectAsync(projectFile); 240 | var comp = await project.GetCompilationAsync(); 241 | if (comp is null) 242 | { 243 | await Console.Error.WriteLineAsync("Could not get compilation"); 244 | return 1; 245 | } 246 | foreach (var sourceTree in comp.SyntaxTrees) 247 | { 248 | SemanticModel sm = comp.GetSemanticModel(sourceTree); 249 | TypesRewriter rewriter = new(sm); 250 | SyntaxNode newSource = rewriter.Visit(await sourceTree.GetRootAsync()); 251 | if (newSource != await sourceTree.GetRootAsync()) 252 | { 253 | await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString()); 254 | } 255 | 256 | UsingRewriter usingRewriter = new(); 257 | var newUsingSource = usingRewriter.Visit(newSource); 258 | if (newUsingSource != newSource) 259 | { 260 | await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString()); 261 | } 262 | 263 | DataProcessorRewriter dataProcessorRewriter = new(); 264 | var dataProcessorSource = dataProcessorRewriter.Visit(newUsingSource); 265 | if (dataProcessorSource != newUsingSource) 266 | { 267 | await File.WriteAllTextAsync(sourceTree.FilePath, dataProcessorSource.ToFullString()); 268 | } 269 | 270 | if ( 271 | sourceTree.FilePath.Contains("/models/", StringComparison.InvariantCultureIgnoreCase) 272 | || sourceTree.FilePath.Contains("\\models\\", StringComparison.InvariantCultureIgnoreCase) 273 | ) 274 | { 275 | // Find all classes that are used in a List 276 | var classNamesInList = dataProcessorSource 277 | .DescendantNodes() 278 | .OfType() 279 | .Where(p => p is { Type: GenericNameSyntax { Identifier.ValueText: "List" } }) 280 | .Select(p => 281 | ((GenericNameSyntax)p.Type) 282 | .TypeArgumentList.Arguments.OfType() 283 | .FirstOrDefault() 284 | ?.Identifier.ValueText 285 | ) 286 | .OfType() 287 | .ToList(); 288 | 289 | var rowIdRewriter = new ModelRewriter(classNamesInList); 290 | var rowIdSource = rowIdRewriter.Visit(dataProcessorSource); 291 | if (rowIdSource != dataProcessorSource) 292 | { 293 | await File.WriteAllTextAsync(sourceTree.FilePath, rowIdSource.ToFullString()); 294 | } 295 | } 296 | } 297 | 298 | await Console.Out.WriteLineAsync("References and using upgraded"); 299 | return 0; 300 | } 301 | 302 | static async Task UpgradeProcess(string processFile) 303 | { 304 | if (!File.Exists(processFile)) 305 | { 306 | await Console.Error.WriteLineAsync( 307 | $"Process file {processFile} does not exist. Please supply location of project with --process [path/to/project.csproj]" 308 | ); 309 | return 1; 310 | } 311 | 312 | await Console.Out.WriteLineAsync("Trying to upgrade process file"); 313 | ProcessUpgrader parser = new(processFile); 314 | parser.Upgrade(); 315 | await parser.Write(); 316 | var warnings = parser.GetWarnings(); 317 | foreach (var warning in warnings) 318 | { 319 | await Console.Out.WriteLineAsync(warning); 320 | } 321 | 322 | await Console.Out.WriteLineAsync( 323 | warnings.Any() 324 | ? "Process file upgraded with warnings. Review the warnings above and make sure that the process file is still valid." 325 | : "Process file upgraded" 326 | ); 327 | 328 | return 0; 329 | } 330 | 331 | static async Task UpgradeAppSettings(string appSettingsFolder) 332 | { 333 | if (!Directory.Exists(appSettingsFolder)) 334 | { 335 | await Console.Error.WriteLineAsync( 336 | $"App settings folder {appSettingsFolder} does not exist. Please supply location with --appsettings-folder [path/to/appsettings]" 337 | ); 338 | return 1; 339 | } 340 | 341 | if ( 342 | Directory.GetFiles(appSettingsFolder, AppSettingsRewriter.AppSettingsRewriter.AppSettingsFilePattern).Length 343 | == 0 344 | ) 345 | { 346 | await Console.Error.WriteLineAsync($"No appsettings*.json files found in {appSettingsFolder}"); 347 | return 1; 348 | } 349 | 350 | await Console.Out.WriteLineAsync("Trying to upgrade appsettings*.json files"); 351 | AppSettingsRewriter.AppSettingsRewriter rewriter = new(appSettingsFolder); 352 | rewriter.Upgrade(); 353 | await rewriter.Write(); 354 | var warnings = rewriter.GetWarnings(); 355 | foreach (var warning in warnings) 356 | { 357 | await Console.Out.WriteLineAsync(warning); 358 | } 359 | 360 | await Console.Out.WriteLineAsync( 361 | warnings.Any() 362 | ? "AppSettings files upgraded with warnings. Review the warnings above and make sure that the appsettings files are still valid." 363 | : "AppSettings files upgraded" 364 | ); 365 | 366 | return 0; 367 | } 368 | 369 | [DoesNotReturn] 370 | private static void ExitWithError(string message, int exitCode = 1) 371 | { 372 | Console.Error.WriteLine(message); 373 | Environment.Exit(exitCode); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/altinn-studio-cli/Upgrade/Frontend/Fev3Tov4/FrontendUpgrade/FrontendUpgrade.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.Checks; 4 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.CustomReceiptRewriter; 5 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.FooterRewriter; 6 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.IndexFileRewriter; 7 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutRewriter; 8 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.LayoutSetRewriter; 9 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.SchemaRefRewriter; 10 | using Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.SettingsWriter; 11 | 12 | namespace Altinn.Studio.Cli.Upgrade.Frontend.Fev3Tov4.FrontendUpgrade; 13 | 14 | internal static class FrontendUpgrade 15 | { 16 | private static void PrintError(string message) 17 | { 18 | Console.ForegroundColor = ConsoleColor.Red; 19 | Console.WriteLine(message); 20 | Console.ResetColor(); 21 | } 22 | 23 | private static void PrintWarning(string message) 24 | { 25 | Console.ForegroundColor = ConsoleColor.Yellow; 26 | Console.WriteLine(message); 27 | Console.ResetColor(); 28 | } 29 | 30 | public static Command GetUpgradeCommand(Option projectFolderOption) 31 | { 32 | var targetVersionOption = new Option( 33 | name: "--target-version", 34 | description: "The target version to upgrade to", 35 | getDefaultValue: () => "4" 36 | ); 37 | var indexFileOption = new Option( 38 | name: "--index-file", 39 | description: "The name of the Index.cshtml file relative to --folder", 40 | getDefaultValue: () => "App/views/Home/Index.cshtml" 41 | ); 42 | var skipIndexFileUpgradeOption = new Option( 43 | name: "--skip-index-file-upgrade", 44 | description: "Skip Index.cshtml upgrade", 45 | getDefaultValue: () => false 46 | ); 47 | var uiFolderOption = new Option( 48 | name: "--ui-folder", 49 | description: "The folder containing layout files relative to --folder", 50 | getDefaultValue: () => "App/ui/" 51 | ); 52 | var textsFolderOption = new Option( 53 | name: "--texts-folder", 54 | description: "The folder containing text files relative to --folder", 55 | getDefaultValue: () => "App/config/texts/" 56 | ); 57 | var layoutSetNameOption = new Option( 58 | name: "--layout-set-name", 59 | description: "The name of the layout set to be created", 60 | getDefaultValue: () => "form" 61 | ); 62 | var applicationMetadataFileOption = new Option( 63 | name: "--application-metadata", 64 | description: "The path of the applicationmetadata.json file relative to --folder", 65 | getDefaultValue: () => "App/config/applicationmetadata.json" 66 | ); 67 | var skipLayoutSetUpgradeOption = new Option( 68 | name: "--skip-layout-set-upgrade", 69 | description: "Skip layout set upgrade", 70 | getDefaultValue: () => false 71 | ); 72 | var skipSettingsUpgradeOption = new Option( 73 | name: "--skip-settings-upgrade", 74 | description: "Skip layout settings upgrade", 75 | getDefaultValue: () => false 76 | ); 77 | var skipLayoutUpgradeOption = new Option( 78 | name: "--skip-layout-upgrade", 79 | description: "Skip layout files upgrade", 80 | getDefaultValue: () => false 81 | ); 82 | var convertGroupTitlesOption = new Option( 83 | name: "--convert-group-titles", 84 | description: "Convert 'title' in repeating groups to 'summaryTitle'", 85 | getDefaultValue: () => false 86 | ); 87 | var skipSchemaRefUpgradeOption = new Option( 88 | name: "--skip-schema-ref-upgrade", 89 | description: "Skip schema reference upgrade", 90 | getDefaultValue: () => false 91 | ); 92 | var skipFooterUpgradeOption = new Option( 93 | name: "--skip-footer-upgrade", 94 | description: "Skip footer upgrade", 95 | getDefaultValue: () => false 96 | ); 97 | var receiptLayoutSetNameOption = new Option( 98 | name: "--receipt-layout-set-name", 99 | description: "The name of the layout set to be created for the custom receipt", 100 | getDefaultValue: () => "receipt" 101 | ); 102 | var skipCustomReceiptUpgradeOption = new Option( 103 | name: "--skip-custom-receipt-upgrade", 104 | description: "Skip custom receipt upgrade", 105 | getDefaultValue: () => false 106 | ); 107 | var skipChecksOption = new Option( 108 | name: "--skip-checks", 109 | description: "Skip checks", 110 | getDefaultValue: () => false 111 | ); 112 | 113 | var upgradeCommand = new Command("frontend", "Upgrade an app from using App-Frontend v3 to v4") 114 | { 115 | projectFolderOption, 116 | targetVersionOption, 117 | indexFileOption, 118 | skipIndexFileUpgradeOption, 119 | uiFolderOption, 120 | textsFolderOption, 121 | layoutSetNameOption, 122 | applicationMetadataFileOption, 123 | skipLayoutSetUpgradeOption, 124 | skipSettingsUpgradeOption, 125 | skipLayoutUpgradeOption, 126 | convertGroupTitlesOption, 127 | skipSchemaRefUpgradeOption, 128 | skipFooterUpgradeOption, 129 | skipCustomReceiptUpgradeOption, 130 | receiptLayoutSetNameOption, 131 | skipChecksOption, 132 | }; 133 | 134 | upgradeCommand.SetHandler( 135 | async (InvocationContext context) => 136 | { 137 | var returnCode = 0; 138 | 139 | // Get simple options 140 | var skipIndexFileUpgrade = context.ParseResult.GetValueForOption(skipIndexFileUpgradeOption); 141 | var skipLayoutSetUpgrade = context.ParseResult.GetValueForOption(skipLayoutSetUpgradeOption); 142 | var skipSettingsUpgrade = context.ParseResult.GetValueForOption(skipSettingsUpgradeOption); 143 | var skipLayoutUpgrade = context.ParseResult.GetValueForOption(skipLayoutUpgradeOption); 144 | var skipSchemaRefUpgrade = context.ParseResult.GetValueForOption(skipSchemaRefUpgradeOption); 145 | var skipFooterUpgrade = context.ParseResult.GetValueForOption(skipFooterUpgradeOption); 146 | var skipCustomReceiptUpgrade = context.ParseResult.GetValueForOption(skipCustomReceiptUpgradeOption); 147 | var skipChecks = context.ParseResult.GetValueForOption(skipChecksOption); 148 | var layoutSetName = context.ParseResult.GetValueForOption(layoutSetNameOption); 149 | var receiptLayoutSetName = context.ParseResult.GetValueForOption(receiptLayoutSetNameOption); 150 | var convertGroupTitles = context.ParseResult.GetValueForOption(convertGroupTitlesOption); 151 | var targetVersion = context.ParseResult.GetValueForOption(targetVersionOption); 152 | 153 | var projectFolder = context.ParseResult.GetValueForOption(projectFolderOption); 154 | if (projectFolder is null) 155 | { 156 | PrintError("Project folder option is required."); 157 | Environment.Exit(1); 158 | return; 159 | } 160 | if (projectFolder == "CurrentDirectory") 161 | { 162 | projectFolder = Directory.GetCurrentDirectory(); 163 | } 164 | if (!Path.IsPathRooted(projectFolder)) 165 | { 166 | projectFolder = Path.Combine(Directory.GetCurrentDirectory(), projectFolder); 167 | } 168 | if (!Directory.Exists(projectFolder)) 169 | { 170 | PrintError( 171 | $"{projectFolder} does not exist. Please supply location of project with --folder [path/to/project]" 172 | ); 173 | Environment.Exit(1); 174 | return; 175 | } 176 | 177 | // Get options requiring project folder 178 | var applicationMetadataFile = context.ParseResult.GetValueForOption(applicationMetadataFileOption); 179 | if (applicationMetadataFile is null) 180 | { 181 | PrintError("Application metadata file option is required."); 182 | Environment.Exit(1); 183 | return; 184 | } 185 | applicationMetadataFile = Path.Combine(projectFolder, applicationMetadataFile); 186 | 187 | var uiFolder = context.ParseResult.GetValueForOption(uiFolderOption); 188 | if (uiFolder is null) 189 | { 190 | PrintError("UI folder option is required."); 191 | Environment.Exit(1); 192 | return; 193 | } 194 | uiFolder = Path.Combine(projectFolder, uiFolder); 195 | 196 | var textsFolder = context.ParseResult.GetValueForOption(textsFolderOption); 197 | if (textsFolder is null) 198 | { 199 | PrintError("Texts folder option is required."); 200 | Environment.Exit(1); 201 | return; 202 | } 203 | textsFolder = Path.Combine(projectFolder, textsFolder); 204 | 205 | var indexFile = context.ParseResult.GetValueForOption(indexFileOption); 206 | if (indexFile is null) 207 | { 208 | PrintError("Index file option is required."); 209 | Environment.Exit(1); 210 | return; 211 | } 212 | indexFile = Path.Combine(projectFolder, indexFile); 213 | 214 | if (!skipIndexFileUpgrade && returnCode == 0) 215 | { 216 | if (targetVersion is null) 217 | { 218 | PrintError("Target version option is required."); 219 | Environment.Exit(1); 220 | return; 221 | } 222 | returnCode = await IndexFileUpgrade(indexFile, targetVersion); 223 | } 224 | 225 | if (!skipLayoutSetUpgrade && returnCode == 0) 226 | { 227 | if (layoutSetName is null) 228 | { 229 | PrintError("Layout set name option is required."); 230 | Environment.Exit(1); 231 | return; 232 | } 233 | returnCode = await LayoutSetUpgrade( 234 | uiFolder, 235 | layoutSetName, 236 | applicationMetadataFile 237 | ); 238 | } 239 | 240 | if (!skipCustomReceiptUpgrade && returnCode == 0) 241 | { 242 | if (receiptLayoutSetName is null) 243 | { 244 | PrintError("Receipt layout set name option is required."); 245 | Environment.Exit(1); 246 | return; 247 | } 248 | returnCode = await CustomReceiptUpgrade(uiFolder, receiptLayoutSetName); 249 | } 250 | 251 | if (!skipSettingsUpgrade && returnCode == 0) 252 | { 253 | returnCode = await CreateMissingSettings(uiFolder); 254 | } 255 | 256 | if (!skipLayoutUpgrade && returnCode == 0) 257 | { 258 | returnCode = await LayoutUpgrade(uiFolder, convertGroupTitles); 259 | } 260 | 261 | if (!skipFooterUpgrade && returnCode == 0) 262 | { 263 | returnCode = await FooterUpgrade(uiFolder); 264 | } 265 | 266 | if (!skipSchemaRefUpgrade && returnCode == 0) 267 | { 268 | if (targetVersion is null) 269 | { 270 | PrintError("Target version option is required."); 271 | Environment.Exit(1); 272 | return; 273 | } 274 | returnCode = await SchemaRefUpgrade( 275 | targetVersion, 276 | uiFolder, 277 | applicationMetadataFile, 278 | textsFolder 279 | ); 280 | } 281 | 282 | if (!skipChecks && returnCode == 0) 283 | { 284 | returnCode = RunChecks(textsFolder); 285 | } 286 | 287 | Environment.Exit(returnCode); 288 | } 289 | ); 290 | 291 | return upgradeCommand; 292 | } 293 | 294 | private static async Task IndexFileUpgrade(string indexFile, string targetVersion) 295 | { 296 | if (!File.Exists(indexFile)) 297 | { 298 | PrintError( 299 | $"Index.cshtml file {indexFile} does not exist. Please supply location of project with --index-file [path/to/Index.cshtml]" 300 | ); 301 | return 1; 302 | } 303 | 304 | var rewriter = new IndexFileUpgrader(indexFile, targetVersion); 305 | rewriter.Upgrade(); 306 | await rewriter.Write(); 307 | 308 | var warnings = rewriter.GetWarnings(); 309 | foreach (var warning in warnings) 310 | { 311 | PrintWarning(warning); 312 | } 313 | 314 | Console.WriteLine( 315 | warnings.Any() ? "Index.cshtml upgraded with warnings. Review the warnings above." : "Index.cshtml upgraded" 316 | ); 317 | return 0; 318 | } 319 | 320 | private static async Task LayoutSetUpgrade( 321 | string uiFolder, 322 | string layoutSetName, 323 | string applicationMetadataFile 324 | ) 325 | { 326 | if (File.Exists(Path.Combine(uiFolder, "layout-sets.json"))) 327 | { 328 | Console.WriteLine("Project already using layout sets. Skipping layout set upgrade."); 329 | return 0; 330 | } 331 | 332 | if (!Directory.Exists(uiFolder)) 333 | { 334 | PrintError( 335 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 336 | ); 337 | return 1; 338 | } 339 | 340 | if (!File.Exists(applicationMetadataFile)) 341 | { 342 | PrintError( 343 | $"Application metadata file {applicationMetadataFile} does not exist. Please supply location of project with --application-metadata [path/to/applicationmetadata.json]" 344 | ); 345 | return 1; 346 | } 347 | 348 | var rewriter = new LayoutSetUpgrader(uiFolder, layoutSetName, applicationMetadataFile); 349 | rewriter.Upgrade(); 350 | await rewriter.Write(); 351 | 352 | var warnings = rewriter.GetWarnings(); 353 | foreach (var warning in warnings) 354 | { 355 | PrintWarning(warning); 356 | } 357 | Console.WriteLine( 358 | warnings.Any() ? "Layout-sets upgraded with warnings. Review the warnings above." : "Layout sets upgraded" 359 | ); 360 | return 0; 361 | } 362 | 363 | private static async Task CustomReceiptUpgrade(string uiFolder, string receiptLayoutSetName) 364 | { 365 | if (!Directory.Exists(uiFolder)) 366 | { 367 | PrintError( 368 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 369 | ); 370 | return 1; 371 | } 372 | 373 | if (!File.Exists(Path.Combine(uiFolder, "layout-sets.json"))) 374 | { 375 | PrintError("Converting to layout sets is required before upgrading custom receipt."); 376 | return 1; 377 | } 378 | 379 | if (Directory.Exists(Path.Combine(uiFolder, receiptLayoutSetName))) 380 | { 381 | Console.WriteLine( 382 | $"A layout set with the name {receiptLayoutSetName} already exists. Skipping custom receipt upgrade." 383 | ); 384 | return 0; 385 | } 386 | 387 | var rewriter = new CustomReceiptUpgrader(uiFolder, receiptLayoutSetName); 388 | rewriter.Upgrade(); 389 | await rewriter.Write(); 390 | 391 | var warnings = rewriter.GetWarnings(); 392 | foreach (var warning in warnings) 393 | { 394 | PrintWarning(warning); 395 | } 396 | Console.WriteLine( 397 | warnings.Any() 398 | ? "Custom receipt upgraded with warnings. Review the warnings above." 399 | : "Custom receipt upgraded" 400 | ); 401 | return 0; 402 | } 403 | 404 | private static async Task CreateMissingSettings(string uiFolder) 405 | { 406 | if (!Directory.Exists(uiFolder)) 407 | { 408 | PrintError( 409 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 410 | ); 411 | return 1; 412 | } 413 | 414 | if (!File.Exists(Path.Combine(uiFolder, "layout-sets.json"))) 415 | { 416 | PrintError("Converting to layout sets is required before upgrading settings."); 417 | return 1; 418 | } 419 | 420 | var rewriter = new SettingsCreator(uiFolder); 421 | rewriter.Upgrade(); 422 | await rewriter.Write(); 423 | 424 | var warnings = rewriter.GetWarnings(); 425 | foreach (var warning in warnings) 426 | { 427 | PrintWarning(warning); 428 | } 429 | Console.WriteLine( 430 | warnings.Any() 431 | ? "Layout settings upgraded with warnings. Review the warnings above." 432 | : "Layout settings upgraded" 433 | ); 434 | return 0; 435 | } 436 | 437 | private static async Task LayoutUpgrade(string uiFolder, bool convertGroupTitles) 438 | { 439 | if (!Directory.Exists(uiFolder)) 440 | { 441 | PrintError( 442 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 443 | ); 444 | return 1; 445 | } 446 | 447 | if (!File.Exists(Path.Combine(uiFolder, "layout-sets.json"))) 448 | { 449 | PrintError("Converting to layout sets is required before upgrading layouts."); 450 | return 1; 451 | } 452 | 453 | var rewriter = new LayoutUpgrader(uiFolder, convertGroupTitles); 454 | rewriter.Upgrade(); 455 | await rewriter.Write(); 456 | 457 | var warnings = rewriter.GetWarnings(); 458 | foreach (var warning in warnings) 459 | { 460 | PrintWarning(warning); 461 | } 462 | 463 | Console.WriteLine( 464 | warnings.Any() ? "Layout files upgraded with warnings. Review the warnings above." : "Layout files upgraded" 465 | ); 466 | return 0; 467 | } 468 | 469 | private static async Task FooterUpgrade(string uiFolder) 470 | { 471 | if (!Directory.Exists(uiFolder)) 472 | { 473 | PrintError( 474 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 475 | ); 476 | return 1; 477 | } 478 | 479 | var rewriter = new FooterUpgrader(uiFolder); 480 | rewriter.Upgrade(); 481 | await rewriter.Write(); 482 | 483 | var warnings = rewriter.GetWarnings(); 484 | foreach (var warning in warnings) 485 | { 486 | PrintWarning(warning); 487 | } 488 | 489 | Console.WriteLine( 490 | warnings.Any() ? "Footer upgraded with warnings. Review the warnings above." : "Footer upgraded" 491 | ); 492 | return 0; 493 | } 494 | 495 | private static async Task SchemaRefUpgrade( 496 | string targetVersion, 497 | string uiFolder, 498 | string applicationMetadataFile, 499 | string textsFolder 500 | ) 501 | { 502 | if (!Directory.Exists(uiFolder)) 503 | { 504 | PrintError( 505 | $"Ui folder {uiFolder} does not exist. Please supply location of project with --ui-folder [path/to/ui/]" 506 | ); 507 | return 1; 508 | } 509 | 510 | if (!Directory.Exists(textsFolder)) 511 | { 512 | PrintError( 513 | $"Texts folder {textsFolder} does not exist. Please supply location of project with --texts-folder [path/to/texts/]" 514 | ); 515 | return 1; 516 | } 517 | 518 | if (!File.Exists(Path.Combine(uiFolder, "layout-sets.json"))) 519 | { 520 | PrintError("Converting to layout sets is required before upgrading schema refereces."); 521 | return 1; 522 | } 523 | 524 | if (!File.Exists(applicationMetadataFile)) 525 | { 526 | PrintError( 527 | $"Application metadata file {applicationMetadataFile} does not exist. Please supply location of project with --application-metadata [path/to/applicationmetadata.json]" 528 | ); 529 | return 1; 530 | } 531 | 532 | var rewriter = new SchemaRefUpgrader(targetVersion, uiFolder, applicationMetadataFile, textsFolder); 533 | rewriter.Upgrade(); 534 | await rewriter.Write(); 535 | 536 | var warnings = rewriter.GetWarnings(); 537 | foreach (var warning in warnings) 538 | { 539 | PrintWarning(warning); 540 | } 541 | 542 | Console.WriteLine( 543 | warnings.Any() 544 | ? "Schema references upgraded with warnings. Review the warnings above." 545 | : "Schema references upgraded" 546 | ); 547 | return 0; 548 | } 549 | 550 | private static int RunChecks(string textsFolder) 551 | { 552 | if (!Directory.Exists(textsFolder)) 553 | { 554 | PrintError( 555 | $"Texts folder {textsFolder} does not exist. Please supply location of project with --texts-folder [path/to/texts/]" 556 | ); 557 | return 1; 558 | } 559 | 560 | Console.WriteLine("Running checks..."); 561 | var checker = new Checker(textsFolder); 562 | 563 | checker.CheckTextDataModelReferences(); 564 | 565 | var warnings = checker.GetWarnings(); 566 | foreach (var warning in warnings) 567 | { 568 | PrintWarning(warning); 569 | } 570 | 571 | Console.WriteLine( 572 | warnings.Any() 573 | ? "Checks finished with warnings. Review the warnings above." 574 | : "Checks finished without warnings" 575 | ); 576 | return 0; 577 | } 578 | } 579 | --------------------------------------------------------------------------------