diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/Inquiry.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/Inquiry.cs
new file mode 100644
index 0000000..654a557
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/Inquiry.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Inquiry
+{
+ public class Inquiry
+ {
+ public String Username { get; set; }
+ public String Recipient { get; set; }
+ public String Text { get; set; }
+
+ public override String ToString()
+ {
+ return "Inquiry{" +
+ "username='" + Username + '\'' +
+ ", recipient='" + Recipient + '\'' +
+ ", text='" + Text + '\'' +
+ '}';
+ }
+
+ public override bool Equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || GetType() != o.GetType()) return false;
+ Inquiry inquiry = (Inquiry)o;
+ return Object.Equals(Username, inquiry.Username) &&
+ Object.Equals(Recipient, inquiry.Recipient) &&
+ Object.Equals(Text, inquiry.Text);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Username, Recipient, Text);
+ }
+ }
+
+
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/InquiryService.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/InquiryService.cs
new file mode 100644
index 0000000..8babfb6
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/InquiryService.cs
@@ -0,0 +1,14 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Inquiry
+{
+ public class InquiryService
+ {
+ public void Create(Inquiry inquiry)
+ {
+ Console.WriteLine(string.Format("User sent inquiry: {0}", inquiry.ToString()));
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/MHP.CodingChallenge.Backend.Dependency.Inquiry.csproj b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/MHP.CodingChallenge.Backend.Dependency.Inquiry.csproj
new file mode 100644
index 0000000..d97cff2
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Inquiry/MHP.CodingChallenge.Backend.Dependency.Inquiry.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net5.0
+
+
+
+
+
+
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/EmailHandler.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/EmailHandler.cs
new file mode 100644
index 0000000..6017b2c
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/EmailHandler.cs
@@ -0,0 +1,14 @@
+using System;
+using MHP.CodingChallenge.Backend.Dependency.Inquiry;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Notifications
+{
+ public class EmailHandler
+ {
+ public virtual void SendEmail(Inquiry.Inquiry inquiry)
+ {
+ Console.WriteLine(string.Format("sending email for: {0}", inquiry.ToString()));
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/MHP.CodingChallenge.Backend.Dependency.Notifications.csproj b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/MHP.CodingChallenge.Backend.Dependency.Notifications.csproj
new file mode 100644
index 0000000..469a3ad
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/MHP.CodingChallenge.Backend.Dependency.Notifications.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/PushNotificationHandler.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/PushNotificationHandler.cs
new file mode 100644
index 0000000..430803f
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Notifications/PushNotificationHandler.cs
@@ -0,0 +1,14 @@
+using System;
+using MHP.CodingChallenge.Backend.Dependency.Inquiry;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Notifications
+{
+ public class PushNotificationHandler
+ {
+ public virtual void SendNotification(Inquiry.Inquiry inquiry)
+ {
+ Console.WriteLine(string.Format("sending notification inquiry: {0}", inquiry.ToString()));
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/InquiryTest.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/InquiryTest.cs
new file mode 100644
index 0000000..cac7980
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/InquiryTest.cs
@@ -0,0 +1,42 @@
+using Xunit;
+using MHP.CodingChallenge.Backend.Dependency.Inquiry;
+using Microsoft.Extensions.DependencyInjection;
+using MHP.CodingChallenge.Backend.Dependency.Notifications;
+using Moq;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Test
+{
+ public class InquiryTest
+ {
+ [Fact]
+ public void TestInquiryHandlers()
+ {
+ // given
+ Inquiry.Inquiry inquiry = new Inquiry.Inquiry();
+ inquiry.Username = "TestUser";
+ inquiry.Recipient = "service@example.com";
+ inquiry.Text = "Can I haz cheezburger?";
+
+ // room for potential additional test setup
+ var mockEmailHander = new Mock();
+ var mockPushNotificationHandler = new Mock();
+
+ var services = new ServiceCollection()
+ .AddLogging()
+ .AddSingleton()
+ .AddSingleton(mockEmailHander.Object)
+ .AddSingleton(mockPushNotificationHandler.Object);
+
+ var inquiryService = services
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ // when
+ inquiryService.Create(inquiry);
+
+ // then
+ mockEmailHander.Verify(e => e.SendEmail(inquiry));
+ mockPushNotificationHandler.Verify(e => e.SendNotification(inquiry));
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/MHP.CodingChallenge.Backend.Dependency.Test.csproj b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/MHP.CodingChallenge.Backend.Dependency.Test.csproj
new file mode 100644
index 0000000..8097150
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/MHP.CodingChallenge.Backend.Dependency.Test.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/Startup.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/Startup.cs
new file mode 100644
index 0000000..72c00a2
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.Test/Startup.cs
@@ -0,0 +1,13 @@
+using System;
+using Microsoft.Extensions.Configuration;
+
+namespace MHP.CodingChallenge.Backend.Dependency.Test
+{
+ public class Startup
+ {
+ public Startup()
+ {
+
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.sln b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.sln
new file mode 100644
index 0000000..757c13c
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.808.8
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Dependency", "MHP.CodingChallenge.Backend.Dependency\MHP.CodingChallenge.Backend.Dependency.csproj", "{3EB0148C-F094-4D4E-A62E-0F722D4EB4BF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Dependency.Inquiry", "MHP.CodingChallenge.Backend.Dependency.Inquiry\MHP.CodingChallenge.Backend.Dependency.Inquiry.csproj", "{D9294E3E-2D76-46E6-A73A-08B942B4A587}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Dependency.Notifications", "MHP.CodingChallenge.Backend.Dependency.Notifications\MHP.CodingChallenge.Backend.Dependency.Notifications.csproj", "{17A527BA-8779-4094-8584-4DEDB8F01235}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Dependency.Test", "MHP.CodingChallenge.Backend.Dependency.Test\MHP.CodingChallenge.Backend.Dependency.Test.csproj", "{97E9E997-33F1-4AAE-A0C8-2FA38E92BFFA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{18C9722F-605C-41BA-ADE0-9627F10E2866}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3EB0148C-F094-4D4E-A62E-0F722D4EB4BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3EB0148C-F094-4D4E-A62E-0F722D4EB4BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3EB0148C-F094-4D4E-A62E-0F722D4EB4BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3EB0148C-F094-4D4E-A62E-0F722D4EB4BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D9294E3E-2D76-46E6-A73A-08B942B4A587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D9294E3E-2D76-46E6-A73A-08B942B4A587}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D9294E3E-2D76-46E6-A73A-08B942B4A587}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D9294E3E-2D76-46E6-A73A-08B942B4A587}.Release|Any CPU.Build.0 = Release|Any CPU
+ {17A527BA-8779-4094-8584-4DEDB8F01235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {17A527BA-8779-4094-8584-4DEDB8F01235}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {17A527BA-8779-4094-8584-4DEDB8F01235}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {17A527BA-8779-4094-8584-4DEDB8F01235}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97E9E997-33F1-4AAE-A0C8-2FA38E92BFFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97E9E997-33F1-4AAE-A0C8-2FA38E92BFFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97E9E997-33F1-4AAE-A0C8-2FA38E92BFFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97E9E997-33F1-4AAE-A0C8-2FA38E92BFFA}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {64806438-BF57-44F0-B7D2-99E7B7A3EF2E}
+ EndGlobalSection
+EndGlobal
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/MHP.CodingChallenge.Backend.Dependency.csproj b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/MHP.CodingChallenge.Backend.Dependency.csproj
new file mode 100644
index 0000000..7daba55
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/MHP.CodingChallenge.Backend.Dependency.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Program.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Program.cs
new file mode 100644
index 0000000..cea4678
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Dependency
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Properties/launchSettings.json b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Properties/launchSettings.json
new file mode 100644
index 0000000..7a6f024
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:7401",
+ "sslPort": 44326
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "MHP.CodingChallenge.Backend.Dependency": {
+ "commandName": "Project",
+ "dotnetRunMessages": "true",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Startup.cs b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Startup.cs
new file mode 100644
index 0000000..9777875
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/Startup.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MHP.CodingChallenge.Backend.Dependency.Inquiry;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.HttpsPolicy;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.OpenApi.Models;
+
+namespace MHP.CodingChallenge.Backend.Dependency
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddTransient();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.Development.json b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.Development.json
new file mode 100644
index 0000000..8983e0f
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.json b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.json
new file mode 100644
index 0000000..d9d9a9b
--- /dev/null
+++ b/Backend/dependency-dotnet/MHP.CodingChallenge.Backend.Dependency/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Backend/dependency-dotnet/README.md b/Backend/dependency-dotnet/README.md
new file mode 100644
index 0000000..18461aa
--- /dev/null
+++ b/Backend/dependency-dotnet/README.md
@@ -0,0 +1,19 @@
+# Backend Coding Challenge: Module Dependency Challenge
+
+### ProjectStructure
+This ASP.NET Core solution consists of three projects (the main application, `Notification` and `Inquiry`).
+The `Notification`-project depends on the `Inquiry`-project. The application depends on both and serves the main ASP.NET entry point.
+
+The `InquiryTest` calls `InquiryService#create(Inquiry)` and checks wether the methods `EmailHandler#sendEmail(Inquiry)`
+and `PushNotificationHandler#sendNotification` have been called with the same parameters.
+
+### Acceptance Criteria:
+- After an Inquiry has been created, `EmailHandler#sendEmail(Inquiry)` and `PushNotificationHandler#sendNotification` have to be executed
+- the `InquiryTest` has to be successful
+
+### general conditions:
+- the classes `Inquiry` shall not be modified
+- the existing classes shall not be moved between the modules
+- the dependencies between the modules shall not be customized
+- Any other NuGet dependencies can be added
+- In case there are neccessary modifications to the test, these require explanation
\ No newline at end of file
diff --git a/Backend/dependency-kotlin/.gitignore b/Backend/dependency-kotlin/.gitignore
new file mode 100644
index 0000000..6790f81
--- /dev/null
+++ b/Backend/dependency-kotlin/.gitignore
@@ -0,0 +1,26 @@
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/Backend/dependency-kotlin/README.md b/Backend/dependency-kotlin/README.md
new file mode 100644
index 0000000..21eaffe
--- /dev/null
+++ b/Backend/dependency-kotlin/README.md
@@ -0,0 +1,39 @@
+# Backend Coding Challenge: Module Dependency Challenge
+
+### ProjectStructure
+This Spring-Boot-Project consists of three Submodules (`inquiry`, `notification` and `application`).
+The `notification`-Module depends on the `inquiry`-Module. The `application` depends on both and serves as Spring boot main module.
+
+The `InquiryTest` calls `InquiryService#create(Inquiry)` and checks wether the methods `EmailHandler#sendEmail(Inquiry)`
+and `PushNotificationHandler#sendNotification` have been called with the same parameters.
+
+### Acceptance Criteria:
+- After an Inquiry has been created, `EmailHandler#sendEmail(Inquiry)` and `PushNotificationHandler#sendNotification` have to be executed
+- the `InquiryTest` has to be successful
+
+### general conditions:
+- the classes `Inquiry`, `InquiryTest` and `Application` shall not be modified
+- the existing classes shall not be moved between the modules
+- the dependencies between the modules shall not be customized
+- Any other gradle dependencies can be added
+
+
+--- German -----------------------------------------------
+
+### Projektaufbau:
+Dieses Spring-Boot-Projekt besteht aus drei Submodulen (`inquiry`, `notification` und `application`).
+Das `notification`-Modul ist vom `inquiry`-Modul abhängig. Das `application` ist von beiden abhängig und dient als Spring boot Hauptmodul.
+
+Der `InquiryTest` ruft `InquiryService#create(Inquiry)` auf und prüft, ob die Methoden `EmailHandler#sendEmail(Inquiry)`
+und `PushNotificationHandler#sendNotification` mit dem gleichen Parameter aufgerufen wurden.
+
+### Akzeptanzkritieren:
+ - Nach dem eine Inquiry erstellt wird, muss `EmailHandler#sendEmail(Inquiry)` und `PushNotificationHandler#sendNotification` ausgeführt werden
+ - Der `InquiryTest` muss erfolgreich sein
+
+### Rahmenbedingungen:
+ - Die Klassen `Inquiry`, `InquiryTest` und `Application` dürfen nicht modifiziert werden
+ - Die bestehenden Klassen dürfen nicht zwischen den Modulen verschoben werden
+ - Die Abhängigkeiten zwischen den Modulen dürfen nicht angepasst werden
+ - Ansonsten können beliebige gradle dependencies hinzugefügt werden
+
diff --git a/Backend/dependency-kotlin/application/build.gradle.kts b/Backend/dependency-kotlin/application/build.gradle.kts
new file mode 100644
index 0000000..98e5b30
--- /dev/null
+++ b/Backend/dependency-kotlin/application/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("org.springframework.boot")
+}
+
+@Suppress("PropertyName")
+val spring_mockk_version: String by project
+
+dependencies {
+ implementation(project(":notifications"))
+ testImplementation("com.ninja-squad:springmockk:$spring_mockk_version")
+}
diff --git a/Backend/dependency-kotlin/application/src/main/kotlin/com/mhp/coding/challenges/dependency/Application.kt b/Backend/dependency-kotlin/application/src/main/kotlin/com/mhp/coding/challenges/dependency/Application.kt
new file mode 100644
index 0000000..c09b384
--- /dev/null
+++ b/Backend/dependency-kotlin/application/src/main/kotlin/com/mhp/coding/challenges/dependency/Application.kt
@@ -0,0 +1,11 @@
+package com.mhp.coding.challenges.dependency
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+
+@SpringBootApplication
+class Application
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/Backend/dependency-kotlin/application/src/test/kotlin/com/mhp/coding/challenges/dependency/InquiryTest.kt b/Backend/dependency-kotlin/application/src/test/kotlin/com/mhp/coding/challenges/dependency/InquiryTest.kt
new file mode 100644
index 0000000..2d28811
--- /dev/null
+++ b/Backend/dependency-kotlin/application/src/test/kotlin/com/mhp/coding/challenges/dependency/InquiryTest.kt
@@ -0,0 +1,41 @@
+package com.mhp.coding.challenges.dependency
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry
+import com.mhp.coding.challenges.dependency.inquiry.InquiryService
+import com.mhp.coding.challenges.dependency.notifications.EmailHandler
+import com.mhp.coding.challenges.dependency.notifications.PushNotificationHandler
+import com.ninjasquad.springmockk.SpykBean
+import io.mockk.verify
+import io.mockk.verifyAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [Application::class])
+class InquiryTest(
+ @Autowired private val inquiryService: InquiryService,
+) {
+
+ @SpykBean
+ lateinit var emailHandler: EmailHandler
+
+ @SpykBean
+ lateinit var pushNotificationHandler: PushNotificationHandler
+
+ @Test
+ fun testInquiryHandlers() {
+ val inquiry = Inquiry(
+ username = "TestUser",
+ recipient = "service@example.com",
+ text = "Can I haz cheezburger?",
+ ).also { inquiryService.create(it) }
+
+ verifyAll {
+ emailHandler.sendEmail(inquiry)
+ pushNotificationHandler.sendNotification(inquiry)
+ }
+ }
+}
diff --git a/Backend/dependency-kotlin/build.gradle.kts b/Backend/dependency-kotlin/build.gradle.kts
new file mode 100644
index 0000000..0999fa6
--- /dev/null
+++ b/Backend/dependency-kotlin/build.gradle.kts
@@ -0,0 +1,63 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+val kotlinlogger_version: String by project
+
+plugins {
+ kotlin("jvm")
+ id("org.springframework.boot") apply false
+ id("io.spring.dependency-management") apply false
+ kotlin("plugin.spring") apply false
+ id("com.adarshr.test-logger")
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(14))
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ jcenter()
+ }
+
+ apply(plugin = "com.adarshr.test-logger")
+
+ testlogger {
+ setTheme("mocha-parallel")
+ isShowFullStackTraces = false
+ slowThreshold = 1_000
+ }
+}
+
+subprojects {
+ apply(plugin = "org.jetbrains.kotlin.jvm")
+ apply(plugin = "org.jetbrains.kotlin.plugin.spring")
+ apply(plugin = "io.spring.dependency-management")
+
+ dependencies {
+ implementation("org.springframework.boot:spring-boot-starter")
+ implementation("io.github.microutils:kotlin-logging:$kotlinlogger_version")
+ testImplementation("org.springframework.boot:spring-boot-starter-test") {
+ exclude(module = "mockito-core")
+ }
+ }
+
+ tasks {
+ withType {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ apiVersion = "1.4"
+ languageVersion = "1.4"
+ freeCompilerArgs = listOf("-Xjsr305=strict")
+ }
+ }
+ withType {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+ }
+ }
+}
diff --git a/Backend/dependency-kotlin/gradle.properties b/Backend/dependency-kotlin/gradle.properties
new file mode 100644
index 0000000..2de9a0e
--- /dev/null
+++ b/Backend/dependency-kotlin/gradle.properties
@@ -0,0 +1,8 @@
+kotlin.code.style=official
+
+kotlin_version=1.4.21
+spring_boot_version=2.4.0
+spring_dependency_management_version=1.0.10.RELEASE
+kotlinlogger_version=2.0.3
+spring_mockk_version=3.0.0
+test_logger_version=2.1.1
diff --git a/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.jar b/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..758de96
Binary files /dev/null and b/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.properties b/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4d9ca16
--- /dev/null
+++ b/Backend/dependency-kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Backend/dependency-kotlin/gradlew b/Backend/dependency-kotlin/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/Backend/dependency-kotlin/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Backend/dependency-kotlin/gradlew.bat b/Backend/dependency-kotlin/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/Backend/dependency-kotlin/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Backend/dependency-kotlin/inquiry/build.gradle.kts b/Backend/dependency-kotlin/inquiry/build.gradle.kts
new file mode 100644
index 0000000..d74c0eb
--- /dev/null
+++ b/Backend/dependency-kotlin/inquiry/build.gradle.kts
@@ -0,0 +1,6 @@
+@Suppress("PropertyName")
+val spring_boot_version: String by project
+
+dependencyManagement {
+ imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${spring_boot_version}") }
+}
diff --git a/Backend/dependency-kotlin/inquiry/src/main/kotlin/com/mhp/coding/challenges/dependency/inquiry/InquiryService.kt b/Backend/dependency-kotlin/inquiry/src/main/kotlin/com/mhp/coding/challenges/dependency/inquiry/InquiryService.kt
new file mode 100644
index 0000000..634752b
--- /dev/null
+++ b/Backend/dependency-kotlin/inquiry/src/main/kotlin/com/mhp/coding/challenges/dependency/inquiry/InquiryService.kt
@@ -0,0 +1,21 @@
+package com.mhp.coding.challenges.dependency.inquiry
+
+import mu.KotlinLogging
+import org.springframework.stereotype.Component
+
+private val logger = KotlinLogging.logger {}
+
+@Component
+class InquiryService {
+ fun create(inquiry: Inquiry) {
+ logger.info {
+ "User sent inquiry: $inquiry"
+ }
+ }
+}
+
+data class Inquiry(
+ var username: String,
+ var recipient: String,
+ var text: String,
+)
diff --git a/Backend/dependency-kotlin/notifications/build.gradle.kts b/Backend/dependency-kotlin/notifications/build.gradle.kts
new file mode 100644
index 0000000..7407936
--- /dev/null
+++ b/Backend/dependency-kotlin/notifications/build.gradle.kts
@@ -0,0 +1,10 @@
+@Suppress("PropertyName")
+val spring_boot_version: String by project
+
+dependencies {
+ api(project(":inquiry"))
+}
+
+dependencyManagement {
+ imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${spring_boot_version}") }
+}
diff --git a/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/EmailHandler.kt b/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/EmailHandler.kt
new file mode 100644
index 0000000..68b9979
--- /dev/null
+++ b/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/EmailHandler.kt
@@ -0,0 +1,18 @@
+package com.mhp.coding.challenges.dependency.notifications
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry
+import com.mhp.coding.challenges.dependency.notifications.EmailHandler
+import mu.KotlinLogging
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+
+private val logger = KotlinLogging.logger {}
+
+@Component
+class EmailHandler {
+ fun sendEmail(inquiry: Inquiry) {
+ logger.info {
+ "Sending email for: $inquiry"
+ }
+ }
+}
diff --git a/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.kt b/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.kt
new file mode 100644
index 0000000..6591152
--- /dev/null
+++ b/Backend/dependency-kotlin/notifications/src/main/kotlin/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.kt
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.dependency.notifications
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry
+import mu.KotlinLogging
+import org.springframework.stereotype.Component
+
+private val logger = KotlinLogging.logger {}
+
+@Component
+class PushNotificationHandler {
+ fun sendNotification(inquiry: Inquiry) {
+ logger.info {
+ "Sending push notification for: $inquiry"
+ }
+ }
+}
diff --git a/Backend/dependency-kotlin/settings.gradle.kts b/Backend/dependency-kotlin/settings.gradle.kts
new file mode 100644
index 0000000..cfb14c1
--- /dev/null
+++ b/Backend/dependency-kotlin/settings.gradle.kts
@@ -0,0 +1,26 @@
+@file:Suppress("LocalVariableName")
+
+rootProject.name = "dependency-challenge"
+
+pluginManagement {
+ repositories { gradlePluginPortal() }
+
+ val kotlin_version: String by settings
+ val spring_boot_version: String by settings
+ val spring_dependency_management_version: String by settings
+ val test_logger_version: String by settings
+
+ plugins {
+ kotlin("jvm") version kotlin_version
+ kotlin("plugin.spring") version kotlin_version
+ id("org.springframework.boot") version spring_boot_version
+ id("io.spring.dependency-management") version spring_dependency_management_version
+ id("com.adarshr.test-logger") version test_logger_version
+ }
+}
+
+include(
+ "inquiry",
+ "notifications",
+ "application"
+)
diff --git a/Backend/dependency/.gitignore b/Backend/dependency/.gitignore
new file mode 100644
index 0000000..6790f81
--- /dev/null
+++ b/Backend/dependency/.gitignore
@@ -0,0 +1,26 @@
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/Backend/dependency/README.md b/Backend/dependency/README.md
new file mode 100644
index 0000000..d74fc1d
--- /dev/null
+++ b/Backend/dependency/README.md
@@ -0,0 +1,42 @@
+# Backend Coding Challenge: Module Dependency Challenge
+
+### ProjectStructure
+This Spring-Boot-Project consists of three Submodules (`inquiry`, `notification` and `application`).
+The `notification`-Module depends on the `inquiry`-Module. The `application` depends on both and serves as Spring boot main module.
+
+The `InquiryTest` calls `InquiryService#create(Inquiry)` and checks wether the methods `EmailHandler#sendEmail(Inquiry)`
+and `PushNotificationHandler#sendNotification` have been called with the same parameters.
+
+### Acceptance Criteria:
+- After an Inquiry has been created, `EmailHandler#sendEmail(Inquiry)` and `PushNotificationHandler#sendNotification` have to be executed
+- the `InquiryTest` has to be successful
+
+### general conditions:
+- the classes `Inquiry`, `InquiryTest` and `Application` shall not be modified
+- the existing classes shall not be moved between the modules
+- the dependencies between the modules shall not be customized
+- Any other gradle dependencies can be added
+- optional: can be implemented in Kotlin
+
+
+
+--- German -----------------------------------------------
+
+### Projektaufbau:
+Dieses Spring-Boot-Projekt besteht aus drei Submodulen (`inquiry`, `notification` und `application`).
+Das `notification`-Modul ist vom `inquiry`-Modul abhängig. Das `application` ist von beiden abhängig und dient als Spring boot Hauptmodul.
+
+Der `InquiryTest` ruft `InquiryService#create(Inquiry)` auf und prüft, ob die Methoden `EmailHandler#sendEmail(Inquiry)`
+und `PushNotificationHandler#sendNotification` mit dem gleichen Parameter aufgerufen wurden.
+
+### Akzeptanzkritieren:
+ - Nach dem eine Inquiry erstellt wird, muss `EmailHandler#sendEmail(Inquiry)` und `PushNotificationHandler#sendNotification` ausgeführt werden
+ - Der `InquiryTest` muss erfolgreich sein
+
+### Rahmenbedingungen:
+ - Die Klassen `Inquiry`, `InquiryTest` und `Application` dürfen nicht modifiziert werden
+ - Die bestehenden Klassen dürfen nicht zwischen den Modulen verschoben werden
+ - Die Abhängigkeiten zwischen den Modulen dürfen nicht angepasst werden
+ - Ansonsten können beliebige gradle dependencies hinzugefügt werden
+ - optional: die Aufgabe kann in Kotlin umgesetzt werden
+
diff --git a/Backend/dependency/application/build.gradle b/Backend/dependency/application/build.gradle
new file mode 100644
index 0000000..b2c45c1
--- /dev/null
+++ b/Backend/dependency/application/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+ ext { springBootVersion = '2.0.5.RELEASE' }
+ repositories { mavenCentral() }
+ dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") }
+}
+
+plugins {
+ id "io.spring.dependency-management" version "1.0.5.RELEASE"
+}
+
+apply plugin: 'java'
+apply plugin: 'idea'
+apply plugin: 'org.springframework.boot'
+apply plugin: 'io.spring.dependency-management'
+
+bootJar {
+ baseName = 'application'
+ version = '0.0.1-SNAPSHOT'
+}
+sourceCompatibility = 1.8
+
+repositories { mavenCentral() }
+
+dependencies {
+ compile project(':notifications')
+ compile('org.springframework.boot:spring-boot-starter-actuator')
+ compile('org.springframework.boot:spring-boot-starter-web')
+ testCompile('org.springframework.boot:spring-boot-starter-test')
+}
diff --git a/Backend/dependency/application/src/main/java/com/mhp/coding/challenges/dependency/Application.java b/Backend/dependency/application/src/main/java/com/mhp/coding/challenges/dependency/Application.java
new file mode 100644
index 0000000..d91e715
--- /dev/null
+++ b/Backend/dependency/application/src/main/java/com/mhp/coding/challenges/dependency/Application.java
@@ -0,0 +1,12 @@
+package com.mhp.coding.challenges.dependency;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/Backend/dependency/application/src/test/java/com/mhp/coding/challenges/dependency/InquiryTest.java b/Backend/dependency/application/src/test/java/com/mhp/coding/challenges/dependency/InquiryTest.java
new file mode 100644
index 0000000..cf9e7b3
--- /dev/null
+++ b/Backend/dependency/application/src/test/java/com/mhp/coding/challenges/dependency/InquiryTest.java
@@ -0,0 +1,42 @@
+package com.mhp.coding.challenges.dependency;
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry;
+import com.mhp.coding.challenges.dependency.inquiry.InquiryService;
+import com.mhp.coding.challenges.dependency.notifications.EmailHandler;
+import com.mhp.coding.challenges.dependency.notifications.PushNotificationHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = Application.class)
+public class InquiryTest {
+
+ @SpyBean
+ private EmailHandler emailHandler;
+
+ @SpyBean
+ private PushNotificationHandler pushNotificationHandler;
+
+ @Autowired
+ private InquiryService inquiryService;
+
+ @Test
+ public void testInquiryHandlers() {
+ final Inquiry inquiry = new Inquiry();
+ inquiry.setUsername("TestUser");
+ inquiry.setRecipient("service@example.com");
+ inquiry.setText("Can I haz cheezburger?");
+
+ inquiryService.create(inquiry);
+
+ verify(emailHandler).sendEmail(eq(inquiry));
+ verify(pushNotificationHandler).sendNotification(eq(inquiry));
+ }
+}
diff --git a/Backend/dependency/gradle/wrapper/gradle-wrapper.jar b/Backend/dependency/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1ce6e58
Binary files /dev/null and b/Backend/dependency/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Backend/dependency/gradle/wrapper/gradle-wrapper.properties b/Backend/dependency/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..448cc64
--- /dev/null
+++ b/Backend/dependency/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Feb 06 12:27:20 CET 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip
diff --git a/Backend/dependency/gradlew b/Backend/dependency/gradlew
new file mode 100755
index 0000000..4453cce
--- /dev/null
+++ b/Backend/dependency/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Backend/dependency/gradlew.bat b/Backend/dependency/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/Backend/dependency/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Backend/dependency/inquiry/build.gradle b/Backend/dependency/inquiry/build.gradle
new file mode 100644
index 0000000..3e96382
--- /dev/null
+++ b/Backend/dependency/inquiry/build.gradle
@@ -0,0 +1,26 @@
+buildscript {
+ repositories { mavenCentral() }
+}
+
+plugins { id "io.spring.dependency-management" version "1.0.5.RELEASE" }
+
+ext { springBootVersion = '2.0.5.RELEASE' }
+
+apply plugin: 'java'
+
+jar {
+ baseName = 'inquiry'
+ version = '0.0.1-SNAPSHOT'
+}
+sourceCompatibility = 1.8
+
+repositories { mavenCentral() }
+
+dependencies {
+ compile('org.springframework.boot:spring-boot-starter')
+ testCompile('org.springframework.boot:spring-boot-starter-test')
+}
+
+dependencyManagement {
+ imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
+}
diff --git a/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/Inquiry.java b/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/Inquiry.java
new file mode 100644
index 0000000..abdcb22
--- /dev/null
+++ b/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/Inquiry.java
@@ -0,0 +1,58 @@
+package com.mhp.coding.challenges.dependency.inquiry;
+
+import java.util.Objects;
+
+public class Inquiry {
+
+ private String username;
+ private String recipient;
+ private String text;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getRecipient() {
+ return recipient;
+ }
+
+ public void setRecipient(String recipient) {
+ this.recipient = recipient;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String toString() {
+ return "Inquiry{" +
+ "username='" + username + '\'' +
+ ", recipient='" + recipient + '\'' +
+ ", text='" + text + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Inquiry inquiry = (Inquiry) o;
+ return Objects.equals(username, inquiry.username) &&
+ Objects.equals(recipient, inquiry.recipient) &&
+ Objects.equals(text, inquiry.text);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(username, recipient, text);
+ }
+}
diff --git a/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/InquiryService.java b/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/InquiryService.java
new file mode 100644
index 0000000..9c8ceb7
--- /dev/null
+++ b/Backend/dependency/inquiry/src/main/java/com/mhp/coding/challenges/dependency/inquiry/InquiryService.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.dependency.inquiry;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class InquiryService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(InquiryService.class);
+
+ public void create(final Inquiry inquiry) {
+ LOG.info("User sent inquiry: {}", inquiry);
+ }
+
+}
diff --git a/Backend/dependency/notifications/build.gradle b/Backend/dependency/notifications/build.gradle
new file mode 100644
index 0000000..74bfc56
--- /dev/null
+++ b/Backend/dependency/notifications/build.gradle
@@ -0,0 +1,27 @@
+buildscript {
+ repositories { mavenCentral() }
+}
+
+plugins { id "io.spring.dependency-management" version "1.0.5.RELEASE" }
+
+ext { springBootVersion = '2.0.5.RELEASE' }
+
+apply plugin: 'java'
+
+jar {
+ baseName = 'notification-library'
+ version = '0.0.1-SNAPSHOT'
+}
+sourceCompatibility = 1.8
+
+repositories { mavenCentral() }
+
+dependencies {
+ compile project(":inquiry")
+ compile('org.springframework.boot:spring-boot-starter')
+ testCompile('org.springframework.boot:spring-boot-starter-test')
+}
+
+dependencyManagement {
+ imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
+}
diff --git a/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/EmailHandler.java b/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/EmailHandler.java
new file mode 100644
index 0000000..428b552
--- /dev/null
+++ b/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/EmailHandler.java
@@ -0,0 +1,17 @@
+package com.mhp.coding.challenges.dependency.notifications;
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class EmailHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EmailHandler.class);
+
+ public void sendEmail(final Inquiry inquiry) {
+ LOG.info("Sending email for: {}", inquiry);
+ }
+
+}
diff --git a/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.java b/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.java
new file mode 100644
index 0000000..698e0bf
--- /dev/null
+++ b/Backend/dependency/notifications/src/main/java/com/mhp/coding/challenges/dependency/notifications/PushNotificationHandler.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.dependency.notifications;
+
+import com.mhp.coding.challenges.dependency.inquiry.Inquiry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PushNotificationHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PushNotificationHandler.class);
+
+ public void sendNotification(final Inquiry inquiry) {
+ LOG.info("Sending push notification for: {}", inquiry);
+ }
+}
diff --git a/Backend/dependency/settings.gradle b/Backend/dependency/settings.gradle
new file mode 100644
index 0000000..7f20038
--- /dev/null
+++ b/Backend/dependency/settings.gradle
@@ -0,0 +1,5 @@
+rootProject.name = 'dependency-challenge'
+include 'inquiry'
+include 'notifications'
+include 'application'
+
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleMapper.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleMapper.cs
new file mode 100644
index 0000000..1fe854a
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleMapper.cs
@@ -0,0 +1,21 @@
+using System;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB;
+using MHP.CodingChallenge.Backend.Mapping.Data.DTO;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data
+{
+ public class ArticleMapper
+ {
+ public ArticleDto Map(Article article)
+ {
+ //TODO
+ return new ArticleDto();
+ }
+
+ public Article Map(ArticleDto articleDto)
+ {
+ // Nicht Teil dieser Challenge.
+ return new Article();
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleService.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleService.cs
new file mode 100644
index 0000000..d584867
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/ArticleService.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB;
+using MHP.CodingChallenge.Backend.Mapping.Data.DTO;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data
+{
+ public class ArticleService
+ {
+ private ArticleRepository _articleRepository;
+ private ArticleMapper _articleMapper;
+
+ public ArticleService(ArticleRepository articleRepository, ArticleMapper articleMapper)
+ {
+ _articleRepository = articleRepository;
+ _articleMapper = articleMapper;
+ }
+
+ public List GetAll()
+ {
+ List articles = _articleRepository.GetAll();
+ // TODO
+ return new List();
+ }
+
+ public object GetById(long id)
+ {
+ Article article = _articleRepository.FindById(id);
+ // TODO
+ return new ArticleDto();
+ }
+
+ public object Create(ArticleDto articleDto)
+ {
+ Article create = _articleMapper.Map(articleDto);
+ _articleRepository.Create(create);
+ return _articleMapper.Map(create);
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Article.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Article.cs
new file mode 100644
index 0000000..ae6b7f9
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Article.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB
+{
+ public class Article : DbEntity
+ {
+ public String Title { get; set; }
+
+ public String Description { get; set; }
+
+ public String Author { get; set; }
+
+ public HashSet Blocks { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ArticleRepository.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ArticleRepository.cs
new file mode 100644
index 0000000..7179d9c
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ArticleRepository.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB
+{
+ public class ArticleRepository
+ {
+ public List GetAll()
+ {
+ List result = new List();
+ result.Add(CreateDummyArticle(1001L));
+ result.Add(CreateDummyArticle(2002L));
+ result.Add(CreateDummyArticle(3003L));
+ result.Add(CreateDummyArticle(4004L));
+ result.Add(CreateDummyArticle(5005L));
+ return result;
+ }
+
+ public Article FindById(long id)
+ {
+ return CreateDummyArticle(id);
+ }
+
+ public void Create(Article article)
+ {
+ //Ignore
+ }
+
+ private Article CreateDummyArticle(long id)
+ {
+ Article result = new Article();
+ result.Id = id;
+ result.Author = "Max Mustermann";
+ result.Description = "Article Description " + id;
+ result.Title = "Article Nr.: " + id;
+ result.LastModifiedBy = "Hans Müller";
+ result.LastModified = DateTime.Now;
+ result.Blocks = CreateBlocks(id);
+ return result;
+ }
+
+ private HashSet CreateBlocks(long articleId)
+ {
+ HashSet result = new HashSet();
+
+ TextBlock textBlock = new TextBlock();
+ textBlock.Text = "Some Text for " + articleId;
+ textBlock.SortIndex = 0;
+ result.Add(textBlock);
+
+ ImageBlock imageBlock = new ImageBlock();
+ imageBlock.Image = CreateImage(1L);
+ textBlock.SortIndex = 1;
+ result.Add(imageBlock);
+
+ TextBlock secondTextBlock = new TextBlock();
+ secondTextBlock.Text = "Second Text for " + articleId;
+ secondTextBlock.SortIndex = 2;
+ result.Add(secondTextBlock);
+
+ GalleryBlock galleryBlock = new GalleryBlock();
+ secondTextBlock.SortIndex = 3;
+
+ List galleryImages = new List();
+ galleryImages.Add(CreateImage(2L));
+ galleryImages.Add(CreateImage(3L));
+ galleryBlock.Images = galleryImages;
+
+ result.Add(galleryBlock);
+
+ TextBlock thirdTextBlock = new TextBlock();
+ thirdTextBlock.Text = "Third Text for " + articleId;
+ thirdTextBlock.SortIndex = 4;
+ result.Add(thirdTextBlock);
+
+ VideoBlock videoBlock = new VideoBlock();
+ videoBlock.Type = VideoBlockType.YOUTUBE;
+ videoBlock.Url = "https://youtu.be/myvideo";
+ videoBlock.SortIndex = 5;
+
+ result.Add(videoBlock);
+
+ return result;
+ }
+
+ private Image CreateImage(long imageId)
+ {
+ Image result = new Image();
+ result.Id = imageId;
+ result.LastModified = DateTime.Now;
+ result.LastModifiedBy = "Max Mustermann";
+ result.ImageSize = ImageSize.LARGE;
+ result.Url = "https://someurl.com/image/" + imageId;
+ return null;
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ArticleBlock.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ArticleBlock.cs
new file mode 100644
index 0000000..94575b3
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ArticleBlock.cs
@@ -0,0 +1,7 @@
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public class ArticleBlock
+ {
+ public int SortIndex { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/GalleryBlock.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/GalleryBlock.cs
new file mode 100644
index 0000000..433ec2d
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/GalleryBlock.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public class GalleryBlock : ArticleBlock
+ {
+ public List Images { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ImageBlock.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ImageBlock.cs
new file mode 100644
index 0000000..b4575e3
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/ImageBlock.cs
@@ -0,0 +1,8 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public class ImageBlock : ArticleBlock
+ {
+ public Image Image { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/TextBlock.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/TextBlock.cs
new file mode 100644
index 0000000..8aae6a4
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/TextBlock.cs
@@ -0,0 +1,8 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public class TextBlock : ArticleBlock
+ {
+ public String Text { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlock.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlock.cs
new file mode 100644
index 0000000..be1bd5a
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlock.cs
@@ -0,0 +1,9 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public class VideoBlock : ArticleBlock
+ {
+ public String Url { get; set; }
+ public VideoBlockType Type { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlockType.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlockType.cs
new file mode 100644
index 0000000..8fbc021
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Blocks/VideoBlockType.cs
@@ -0,0 +1,9 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks
+{
+ public enum VideoBlockType
+ {
+ YOUTUBE,
+ VIMEO
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/DbEntity.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/DbEntity.cs
new file mode 100644
index 0000000..fd4f8a1
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/DbEntity.cs
@@ -0,0 +1,10 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB
+{
+ public class DbEntity
+ {
+ public long Id { get; set; }
+ public DateTime LastModified { get; set; }
+ public String LastModifiedBy { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Image.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Image.cs
new file mode 100644
index 0000000..27553ed
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/Image.cs
@@ -0,0 +1,9 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB
+{
+ public class Image : DbEntity
+ {
+ public String Url { get; set; }
+ public ImageSize ImageSize { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ImageSize.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ImageSize.cs
new file mode 100644
index 0000000..1fcb4b2
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DB/ImageSize.cs
@@ -0,0 +1,10 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DB
+{
+ public enum ImageSize
+ {
+ SMALL,
+ MEDIUM,
+ LARGE
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleBlockDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleBlockDto.cs
new file mode 100644
index 0000000..95f1549
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleBlockDto.cs
@@ -0,0 +1,8 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO
+{
+ public class ArticleBlockDto
+ {
+ public int SortIndex { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleDto.cs
new file mode 100644
index 0000000..d41fb63
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ArticleDto.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO
+{
+ public class ArticleDto
+ {
+ public long Id { get; set; }
+ public String Title { get; set; }
+ public String Description { get; set; }
+ public String Author { get; set; }
+ public List Blocks { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/GalleryBlockDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/GalleryBlockDto.cs
new file mode 100644
index 0000000..e59e861
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/GalleryBlockDto.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO.Blocks
+{
+ public class GalleryBlockDto : ArticleBlockDto
+ {
+ public List Images { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/ImageBlockDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/ImageBlockDto.cs
new file mode 100644
index 0000000..ef504cb
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/ImageBlockDto.cs
@@ -0,0 +1,7 @@
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO.Blocks
+{
+ public class ImageBlockDto : ArticleBlockDto
+ {
+ public ImageDto Image { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/TextBlockDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/TextBlockDto.cs
new file mode 100644
index 0000000..decb805
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/TextBlockDto.cs
@@ -0,0 +1,8 @@
+using System;
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO.Blocks
+{
+ public class TextBlockDto : ArticleBlockDto
+ {
+ public String Text { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/VideoBlockDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/VideoBlockDto.cs
new file mode 100644
index 0000000..e7f1948
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/Blocks/VideoBlockDto.cs
@@ -0,0 +1,11 @@
+using System;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB.Blocks;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO.Blocks
+{
+ public class VideoBlockDto : ArticleBlockDto
+ {
+ public String Url { get; set; }
+ public VideoBlockType Type { get; set; }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ImageDto.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ImageDto.cs
new file mode 100644
index 0000000..6de809e
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/DTO/ImageDto.cs
@@ -0,0 +1,6 @@
+namespace MHP.CodingChallenge.Backend.Mapping.Data.DTO
+{
+ public class ImageDto
+ {
+ }
+}
\ No newline at end of file
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/IServiceCollectionExtension.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/IServiceCollectionExtension.cs
new file mode 100644
index 0000000..5d7e8e5
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/IServiceCollectionExtension.cs
@@ -0,0 +1,18 @@
+using System;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Data
+{
+ public static class IServiceCollectionExtension
+ {
+ public static IServiceCollection AddDataServices(this IServiceCollection services)
+ {
+ services.AddScoped(typeof(ArticleService));
+ services.AddScoped(typeof(ArticleRepository));
+ services.AddScoped(typeof(ArticleMapper));
+ return services;
+ }
+
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/MHP.CodingChallenge.Backend.Mapping.Data.csproj b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/MHP.CodingChallenge.Backend.Mapping.Data.csproj
new file mode 100644
index 0000000..f4c3abe
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Data/MHP.CodingChallenge.Backend.Mapping.Data.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/MHP.CodingChallenge.Backend.Mapping.Tests.csproj b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/MHP.CodingChallenge.Backend.Mapping.Tests.csproj
new file mode 100644
index 0000000..c0dc1ac
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/MHP.CodingChallenge.Backend.Mapping.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/UnitTest1.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/UnitTest1.cs
new file mode 100644
index 0000000..ebf5831
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Tests
+{
+ public class UnitTest1
+ {
+ [Fact]
+ public void Test()
+ {
+
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.sln b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.sln
new file mode 100644
index 0000000..101998a
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping.sln
@@ -0,0 +1,42 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.808.8
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Mapping", "MHP.CodingChallenge.Backend.Mapping\MHP.CodingChallenge.Backend.Mapping.csproj", "{C2BB5229-98BA-451C-873D-B54325631273}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Mapping.Data", "MHP.CodingChallenge.Backend.Mapping.Data\MHP.CodingChallenge.Backend.Mapping.Data.csproj", "{C5E21D5B-EFF1-4338-B979-7554B6C1119A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MHP.CodingChallenge.Backend.Mapping.Tests", "MHP.CodingChallenge.Backend.Mapping.Tests\MHP.CodingChallenge.Backend.Mapping.Tests.csproj", "{8282DCA3-A7F5-4485-8B9D-23A75C53FA29}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5B99CAB3-F22C-4111-9546-98F3E35C34AA}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C2BB5229-98BA-451C-873D-B54325631273}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2BB5229-98BA-451C-873D-B54325631273}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2BB5229-98BA-451C-873D-B54325631273}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2BB5229-98BA-451C-873D-B54325631273}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5E21D5B-EFF1-4338-B979-7554B6C1119A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5E21D5B-EFF1-4338-B979-7554B6C1119A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5E21D5B-EFF1-4338-B979-7554B6C1119A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5E21D5B-EFF1-4338-B979-7554B6C1119A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8282DCA3-A7F5-4485-8B9D-23A75C53FA29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8282DCA3-A7F5-4485-8B9D-23A75C53FA29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8282DCA3-A7F5-4485-8B9D-23A75C53FA29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8282DCA3-A7F5-4485-8B9D-23A75C53FA29}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3A731A92-258E-4E54-84E2-F860BEBDD953}
+ EndGlobalSection
+EndGlobal
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Controllers/ArticleController.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Controllers/ArticleController.cs
new file mode 100644
index 0000000..878ffc8
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Controllers/ArticleController.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MHP.CodingChallenge.Backend.Mapping.Data;
+using MHP.CodingChallenge.Backend.Mapping.Data.DTO;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Mapping.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class ArticleController : ControllerBase
+ {
+ private ILogger _logger;
+ private ArticleService _articleService;
+
+ public ArticleController(ILogger logger,
+ ArticleService articleService)
+ {
+ _logger = logger;
+ _articleService = articleService;
+ }
+
+ [HttpGet]
+ public IActionResult Get()
+ {
+ return new JsonResult(_articleService.GetAll());
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(long id)
+ {
+ return new JsonResult(_articleService.GetById(id));
+ }
+
+ [HttpPost]
+ public IActionResult Create(ArticleDto article)
+ {
+ return new JsonResult(_articleService.Create(article));
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/MHP.CodingChallenge.Backend.Mapping.csproj b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/MHP.CodingChallenge.Backend.Mapping.csproj
new file mode 100644
index 0000000..6c48f5e
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/MHP.CodingChallenge.Backend.Mapping.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Program.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Program.cs
new file mode 100644
index 0000000..b123952
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MHP.CodingChallenge.Backend.Mapping
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Properties/launchSettings.json b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Properties/launchSettings.json
new file mode 100644
index 0000000..c4ed230
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:59942",
+ "sslPort": 44391
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "MHP.CodingChallenge.Backend.Mapping": {
+ "commandName": "Project",
+ "dotnetRunMessages": "true",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Startup.cs b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Startup.cs
new file mode 100644
index 0000000..6db0f0a
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/Startup.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MHP.CodingChallenge.Backend.Mapping.Data;
+using MHP.CodingChallenge.Backend.Mapping.Data.DB;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.HttpsPolicy;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.OpenApi.Models;
+
+namespace MHP.CodingChallenge.Backend.Mapping
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddControllers();
+ services.AddSwaggerGen(c =>
+ {
+ c.SwaggerDoc("v1", new OpenApiInfo { Title = "MHP.CodingChallenge.Backend.Mapping", Version = "v1" });
+ });
+
+ services.AddDataServices();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ app.UseSwagger();
+ app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MHP.CodingChallenge.Backend.Mapping v1"));
+ }
+
+ app.UseHttpsRedirection();
+
+ app.UseRouting();
+
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.Development.json b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.Development.json
new file mode 100644
index 0000000..8983e0f
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.json b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.json
new file mode 100644
index 0000000..d9d9a9b
--- /dev/null
+++ b/Backend/mapping-dotnet/MHP.CodingChallenge.Backend.Mapping/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Backend/mapping-dotnet/README.md b/Backend/mapping-dotnet/README.md
new file mode 100644
index 0000000..7aa99b8
--- /dev/null
+++ b/Backend/mapping-dotnet/README.md
@@ -0,0 +1,13 @@
+# Backend Coding Challenge: Mapping Challenge
+
+### acceptance criteria:
+ - `Article` is correctly mapped to `ArticleDTO` (see `ArticleController#list` and `ArticleController#details`) and is emitted as a JSON from the Controllers
+ - the collection of `ArticleBlockDto` in `ArticleDTO` is sorted after `sortIndex` in `ArticleBlockDTO`
+ - in case an `Article` cannot be found via ID, a 404 shall be shown (see `ArticleController#details`)
+ - optional: in case a new implementation of `ArticleBlock` is created and no mapping is implemented, the user shall get an info
+
+### general conditions:
+ - DB Models and DTO Models can be extended with Interfaces/Properties
+ - Existing field of Models and DTOs shall not be modified
+ - the package structure shall not be modified
+ - Any other NuGet dependencies can be added
\ No newline at end of file
diff --git a/Backend/mapping-kotlin/.gitignore b/Backend/mapping-kotlin/.gitignore
new file mode 100644
index 0000000..9243c63
--- /dev/null
+++ b/Backend/mapping-kotlin/.gitignore
@@ -0,0 +1,26 @@
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/Backend/mapping-kotlin/README.md b/Backend/mapping-kotlin/README.md
new file mode 100644
index 0000000..515b3f5
--- /dev/null
+++ b/Backend/mapping-kotlin/README.md
@@ -0,0 +1,30 @@
+# Backend Coding Challenge: Mapping Challenge
+
+### acceptance criteria:
+ - `Article` is correctly mapped to `ArticleDTO` (see `ArticleController#list` and `ArticleController#details`) and is emitted as a JSON from the Controllers
+ - the collection of `ArticleBlockDto` in `ArticleDTO` is sorted after `sortIndex` in `ArticleBlockDTO`
+ - in case an `Article` cannot be found via ID, a 404 shall be shown (see `ArticleController#details`)
+ - optional: in case a new implementation of `ArticleBlock` is created and no mapping is implemented, the user shall get an info
+
+### general conditions:
+ - DB Models and DTO Models can be extended with Interfaces/Properties
+ - An Existing field of Models and DTOs shall not be modified
+ - the package structure shall not be modified
+ - Any other gradle dependencies can be added
+
+
+--- German -----------------------------------------------
+
+### Akzeptanzkritieren:
+ - `Article` wird korrekt zu `ArticleDTO` gemapped (Siehe `ArticleController#list` und `ArticleController#details`) und als JSON von den Controllern ausgegeben
+ - Die Collection von `ArticleBlockDto` in `ArticleDTO` ist nach dem `sortIndex` in `ArticleBlockDTO` sortiert
+ - Falls ein `Article` per ID nicht gefunden werden kann, soll eine 404 Repsonse ausgeliefert werden (Siehe `ArticleController#details`)
+ - Optional: Falls eine neue Implementierung/Ableitung von `ArticleBlock` implementiert wird und noch kein Mapping implementiert ist,
+ soll mann darauf hingewiesen werden. Wie ist frei überlassen
+
+### Rahmenbedingungen:
+ - DB Models und DTO Models können mit Interfaces/Properties erweitert werden
+ - Bestehende Felder von Models und DTOs können nicht modifiziert werden
+ - Die Packagestruktur darf nicht modifiziert werden
+ - Es können beliebig gradle dependencies hinzugefügt werden
+
diff --git a/Backend/mapping-kotlin/build.gradle.kts b/Backend/mapping-kotlin/build.gradle.kts
new file mode 100644
index 0000000..6abb099
--- /dev/null
+++ b/Backend/mapping-kotlin/build.gradle.kts
@@ -0,0 +1,38 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ val kotlinVersion = "1.4.21"
+ kotlin("jvm") version kotlinVersion
+ kotlin("plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "2.4.0"
+ id("io.spring.dependency-management") version "1.0.10.RELEASE"
+}
+
+group = "com.mhp.coding.challenges"
+version = "0.0.1-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation("org.springframework.boot:spring-boot-starter-web")
+ testImplementation("org.springframework.boot:spring-boot-starter-test")
+}
+
+tasks {
+ withType {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ apiVersion = "1.4"
+ languageVersion = "1.4"
+ freeCompilerArgs = listOf("-Xjsr305=strict")
+ }
+ }
+ withType {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+ }
+}
diff --git a/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.jar b/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..758de96
Binary files /dev/null and b/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.properties b/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4d9ca16
--- /dev/null
+++ b/Backend/mapping-kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Backend/mapping-kotlin/gradlew b/Backend/mapping-kotlin/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/Backend/mapping-kotlin/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Backend/mapping-kotlin/gradlew.bat b/Backend/mapping-kotlin/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/Backend/mapping-kotlin/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Backend/mapping-kotlin/settings.gradle.kts b/Backend/mapping-kotlin/settings.gradle.kts
new file mode 100644
index 0000000..bfe82a3
--- /dev/null
+++ b/Backend/mapping-kotlin/settings.gradle.kts
@@ -0,0 +1,5 @@
+rootProject.name = "mapping"
+
+pluginManagement {
+ repositories { gradlePluginPortal() }
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/Application.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/Application.kt
new file mode 100644
index 0000000..c571014
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/Application.kt
@@ -0,0 +1,11 @@
+package com.mhp.coding.challenges.mapping
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+
+@SpringBootApplication
+class Application
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/controllers/ArticleController.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/controllers/ArticleController.kt
new file mode 100644
index 0000000..e94c79d
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/controllers/ArticleController.kt
@@ -0,0 +1,20 @@
+package com.mhp.coding.challenges.mapping.controllers
+
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto
+import com.mhp.coding.challenges.mapping.services.ArticleService
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/article")
+class ArticleController(
+ private val articleService: ArticleService
+) {
+ @GetMapping
+ fun list(): List = articleService.list()
+
+ @GetMapping("/{id}")
+ fun details(@PathVariable id: Long): ArticleDto = articleService.articleForId(id)
+
+ @PostMapping
+ fun create(@RequestBody articleDto: ArticleDto): ArticleDto = articleService.create(articleDto)
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.kt
new file mode 100644
index 0000000..ae50103
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.kt
@@ -0,0 +1,22 @@
+package com.mhp.coding.challenges.mapping.mappers
+
+import com.mhp.coding.challenges.mapping.models.db.Article
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto
+import org.springframework.stereotype.Component
+import java.util.*
+
+@Component
+class ArticleMapper {
+ fun map(article: Article?): ArticleDto {
+ //TODO
+ return ArticleDto(0, "", "", "", emptyList())
+ }
+
+ // Not part of the challenge / Nicht Teil dieser Challenge.
+ fun map(articleDto: ArticleDto?): Article = Article(
+ title = "An Article",
+ blocks = emptySet(),
+ id = 1,
+ lastModified = Date()
+ )
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Article.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Article.kt
new file mode 100644
index 0000000..64fc5d2
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Article.kt
@@ -0,0 +1,14 @@
+package com.mhp.coding.challenges.mapping.models.db
+
+import com.mhp.coding.challenges.mapping.models.db.blocks.ArticleBlock
+import java.util.*
+
+class Article(
+ var title: String,
+ var description: String? = null,
+ var author: String? = null,
+ var blocks: Set,
+ override var id: Long,
+ override var lastModified: Date,
+ override var lastModifiedBy: String? = null
+) : DBEntity
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/DBEntity.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/DBEntity.kt
new file mode 100644
index 0000000..1b33e05
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/DBEntity.kt
@@ -0,0 +1,9 @@
+package com.mhp.coding.challenges.mapping.models.db
+
+import java.util.*
+
+interface DBEntity {
+ var id: Long
+ var lastModified: Date
+ var lastModifiedBy: String?
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Image.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Image.kt
new file mode 100644
index 0000000..4215272
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/Image.kt
@@ -0,0 +1,17 @@
+package com.mhp.coding.challenges.mapping.models.db
+
+import java.util.*
+
+class Image(
+ var url: String,
+ var imageSize: ImageSize,
+ override var id: Long,
+ override var lastModified: Date,
+ override var lastModifiedBy: String? = null
+) : DBEntity
+
+enum class ImageSize {
+ SMALL,
+ MEDIUM,
+ LARGE,
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.kt
new file mode 100644
index 0000000..cf296be
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.kt
@@ -0,0 +1,5 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks
+
+open class ArticleBlock(
+ open val sortIndex: Int
+)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.kt
new file mode 100644
index 0000000..14b79f1
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.kt
@@ -0,0 +1,8 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks
+
+import com.mhp.coding.challenges.mapping.models.db.Image
+
+class GalleryBlock(
+ var images: List,
+ override val sortIndex: Int = 0,
+) : ArticleBlock(sortIndex)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.kt
new file mode 100644
index 0000000..b4bc46a
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.kt
@@ -0,0 +1,8 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks
+
+import com.mhp.coding.challenges.mapping.models.db.Image
+
+class ImageBlock(
+ var image: Image?,
+ override val sortIndex: Int = 0,
+) : ArticleBlock(sortIndex)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.kt
new file mode 100644
index 0000000..edae7fd
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.kt
@@ -0,0 +1,6 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks
+
+class TextBlock(
+ var text: String,
+ override var sortIndex: Int = 0,
+) : ArticleBlock(sortIndex)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.kt
new file mode 100644
index 0000000..2173d4a
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.kt
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks
+
+class VideoBlock(
+ var url: String,
+ var type: VideoBlockType,
+ override val sortIndex: Int = 0,
+) : ArticleBlock(sortIndex)
+
+enum class VideoBlockType {
+ YOUTUBE,
+ VIMEO,
+ TWITCH,
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.kt
new file mode 100644
index 0000000..b361962
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.kt
@@ -0,0 +1,11 @@
+package com.mhp.coding.challenges.mapping.models.dto
+
+import com.mhp.coding.challenges.mapping.models.dto.blocks.ArticleBlockDto
+
+data class ArticleDto(
+ var id: Long,
+ var title: String,
+ var description: String,
+ var author: String,
+ var blocks: Collection
+)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ImageDto.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ImageDto.kt
new file mode 100644
index 0000000..a286c74
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/ImageDto.kt
@@ -0,0 +1,9 @@
+package com.mhp.coding.challenges.mapping.models.dto
+
+import com.mhp.coding.challenges.mapping.models.db.ImageSize
+
+data class ImageDto(
+ var id: Long,
+ var url: String,
+ var imageSize: ImageSize,
+)
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.kt
new file mode 100644
index 0000000..7a90f0c
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.kt
@@ -0,0 +1,5 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks
+
+interface ArticleBlockDto {
+ val sortIndex: Int
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.kt
new file mode 100644
index 0000000..85785c8
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.kt
@@ -0,0 +1,8 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks
+
+import com.mhp.coding.challenges.mapping.models.dto.ImageDto
+
+data class GalleryBlockDto(
+ var images: List,
+ override val sortIndex: Int,
+) : ArticleBlockDto
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.kt
new file mode 100644
index 0000000..6f16634
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.kt
@@ -0,0 +1,8 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks
+
+import com.mhp.coding.challenges.mapping.models.dto.ImageDto
+
+data class ImageBlock(
+ var image: ImageDto,
+ override val sortIndex: Int,
+) : ArticleBlockDto
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.kt
new file mode 100644
index 0000000..02c0b68
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.kt
@@ -0,0 +1,6 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks
+
+data class TextBlock(
+ var text: String,
+ override val sortIndex: Int,
+) : ArticleBlockDto
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.kt
new file mode 100644
index 0000000..574779f
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.kt
@@ -0,0 +1,9 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks
+
+import com.mhp.coding.challenges.mapping.models.db.blocks.VideoBlockType
+
+data class VideoBlock(
+ var url: String,
+ var type: VideoBlockType,
+ override val sortIndex: Int,
+) : ArticleBlockDto
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.kt
new file mode 100644
index 0000000..7f425db
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.kt
@@ -0,0 +1,76 @@
+package com.mhp.coding.challenges.mapping.repositories
+
+import com.mhp.coding.challenges.mapping.models.db.Article
+import com.mhp.coding.challenges.mapping.models.db.Image
+import com.mhp.coding.challenges.mapping.models.db.ImageSize
+import com.mhp.coding.challenges.mapping.models.db.blocks.*
+import org.springframework.stereotype.Component
+import java.util.*
+
+object ArticleRepository {
+ fun all(): List = setOf(1001L, 2002L, 3003L, 4004L, 5005L).map { it.createDummyArticle }
+
+ fun findBy(id: Long): Article = id.createDummyArticle
+
+ fun create(article: Article?) {
+ //Ignore
+ }
+
+ private val Long.createDummyArticle
+ get() = Article(
+ id = this,
+ lastModified = Date(),
+ lastModifiedBy = "Hans Müller",
+ title = "Article Nr.: $this",
+ description = "Article Description $this",
+ author = "Max Mustermann",
+ blocks = dummyArticleBlocks,
+ )
+
+ private val Long.dummyArticleBlocks: Set by lazy {
+ val textBlock = TextBlock(
+ text = "Some Text for $this",
+ sortIndex = 0
+ )
+
+ val imageBlock = ImageBlock(
+ image = createImage(1L),
+ sortIndex = 1
+ )
+
+ val secondTextBlock = TextBlock(
+ text = "Second Text for $this",
+ sortIndex = 2
+ ).also { textBlock.sortIndex = 1 }
+
+ val galleryBlock = GalleryBlock(
+ images = listOf(
+ createImage(2L),
+ createImage(3L)
+ )
+ ).also { secondTextBlock.sortIndex = 3 }
+
+ val thirdTextBlock = TextBlock(
+ text = "Third Text for $this",
+ sortIndex = 4
+ )
+
+ val videoBlock = VideoBlock(
+ type = VideoBlockType.YOUTUBE,
+ url = "https://youtu.be/myvideo",
+ sortIndex = 4
+ )
+
+ setOf(textBlock, imageBlock, secondTextBlock, galleryBlock, thirdTextBlock, videoBlock)
+ }
+
+ private fun createImage(imageId: Long): Image? {
+ return Image(
+ url = "https://someurl.com/image/$imageId",
+ id = imageId,
+ imageSize = ImageSize.LARGE,
+ lastModified = Date(),
+ lastModifiedBy = "John Doe"
+ ).let { null }
+ }
+}
diff --git a/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/services/ArticleService.kt b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/services/ArticleService.kt
new file mode 100644
index 0000000..11399bf
--- /dev/null
+++ b/Backend/mapping-kotlin/src/main/kotlin/com/mhp/coding/challenges/mapping/services/ArticleService.kt
@@ -0,0 +1,29 @@
+package com.mhp.coding.challenges.mapping.services
+
+import com.mhp.coding.challenges.mapping.repositories.ArticleRepository
+import com.mhp.coding.challenges.mapping.mappers.ArticleMapper
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto
+import org.springframework.stereotype.Service
+
+@Service
+class ArticleService(
+ private val mapper: ArticleMapper,
+) {
+ fun list(): List {
+ val articles = ArticleRepository.all()
+ //TODO
+ return emptyList()
+ }
+
+ fun articleForId(id: Long): ArticleDto {
+ val article = ArticleRepository.findBy(id)
+ //TODO
+ return ArticleDto(0, "", "", "", emptyList())
+ }
+
+ fun create(articleDto: ArticleDto): ArticleDto {
+ val article = mapper.map(articleDto)
+ ArticleRepository.create(article)
+ return mapper.map(article)
+ }
+}
diff --git a/Backend/mapping-kotlin/src/main/resources/application.properties b/Backend/mapping-kotlin/src/main/resources/application.properties
new file mode 100644
index 0000000..e69de29
diff --git a/Backend/mapping-kotlin/src/test/kotlin/com/mhp/coding/challenges/mapping/ApplicationTests.kt b/Backend/mapping-kotlin/src/test/kotlin/com/mhp/coding/challenges/mapping/ApplicationTests.kt
new file mode 100644
index 0000000..0df3abb
--- /dev/null
+++ b/Backend/mapping-kotlin/src/test/kotlin/com/mhp/coding/challenges/mapping/ApplicationTests.kt
@@ -0,0 +1,14 @@
+package com.mhp.coding.challenges.mapping
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+@ExtendWith(SpringExtension::class)
+@SpringBootTest
+class ApplicationTests {
+ @Test
+ fun contextLoads() {
+ }
+}
diff --git a/Backend/mapping/.gitignore b/Backend/mapping/.gitignore
new file mode 100644
index 0000000..9243c63
--- /dev/null
+++ b/Backend/mapping/.gitignore
@@ -0,0 +1,26 @@
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/Backend/mapping/README.md b/Backend/mapping/README.md
new file mode 100644
index 0000000..a2f3fdc
--- /dev/null
+++ b/Backend/mapping/README.md
@@ -0,0 +1,32 @@
+# Backend Coding Challenge: Mapping Challenge
+
+### acceptance criteria:
+ - `Article` is correctly mapped to `ArticleDTO` (see `ArticleController#list` and `ArticleController#details`) and is emitted as a JSON from the Controllers
+ - the collection of `ArticleBlockDto` in `ArticleDTO` is sorted after `sortIndex` in `ArticleBlockDTO`
+ - in case an `Article` cannot be found via ID, a 404 shall be shown (see `ArticleController#details`)
+ - optional: in case a new implementation of `ArticleBlock` is created and no mapping is implemented, the user shall get an info
+
+### general conditions:
+ - DB Models and DTO Models can be extended with Interfaces/Properties
+ - Existing field of Models and DTOs shall not be modified
+ - the package structure shall not be modified
+ - Any other gradle dependencies can be added
+ - optional: can be implemented in Kotlin
+
+
+
+--- German -----------------------------------------------
+
+### Akzeptanzkritieren:
+ - `Article` wird korrekt zu `ArticleDTO` gemapped (Siehe `ArticleController#list` und `ArticleController#details`) und als JSON von den Controllern ausgegeben
+ - Die Collection von `ArticleBlockDto` in `ArticleDTO` ist nach dem `sortIndex` in `ArticleBlockDTO` sortiert
+ - Falls ein `Article` per ID nicht gefunden werden kann, soll eine 404 Repsonse ausgeliefert werden (Siehe `ArticleController#details`)
+ - Optional: Falls eine neue Implementierung/Ableitung von `ArticleBlock` implementiert wird und noch kein Mapping implementiert ist,
+ soll mann darauf hingewiesen werden. Wie ist frei überlassen
+
+### Rahmenbedingungen:
+ - DB Models und DTO Models können mit Interfaces/Properties erweitert werden
+ - Bestehende Felder von Models und DTOs können nicht modifiziert werden
+ - Die Packagestruktur darf nicht modifiziert werden
+ - Es können beliebig gradle dependencies hinzugefügt werden
+ - optional: die Aufgabe kann in Kotlin umgesetzt werden
diff --git a/Backend/mapping/build.gradle b/Backend/mapping/build.gradle
new file mode 100644
index 0000000..b124b2e
--- /dev/null
+++ b/Backend/mapping/build.gradle
@@ -0,0 +1,30 @@
+buildscript {
+ ext {
+ springBootVersion = '2.0.5.RELEASE'
+ }
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+}
+
+apply plugin: 'java'
+apply plugin: 'eclipse'
+apply plugin: 'org.springframework.boot'
+apply plugin: 'io.spring.dependency-management'
+
+group = 'com.mhp.coding.challenges'
+version = '0.0.1-SNAPSHOT'
+sourceCompatibility = 1.8
+
+repositories {
+ mavenCentral()
+}
+
+
+dependencies {
+ compile('org.springframework.boot:spring-boot-starter-web')
+ testCompile('org.springframework.boot:spring-boot-starter-test')
+}
diff --git a/Backend/mapping/gradle/wrapper/gradle-wrapper.jar b/Backend/mapping/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1ce6e58
Binary files /dev/null and b/Backend/mapping/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Backend/mapping/gradle/wrapper/gradle-wrapper.properties b/Backend/mapping/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..448cc64
--- /dev/null
+++ b/Backend/mapping/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Feb 06 12:27:20 CET 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip
diff --git a/Backend/mapping/gradlew b/Backend/mapping/gradlew
new file mode 100755
index 0000000..4453cce
--- /dev/null
+++ b/Backend/mapping/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Backend/mapping/gradlew.bat b/Backend/mapping/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/Backend/mapping/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Backend/mapping/settings.gradle b/Backend/mapping/settings.gradle
new file mode 100644
index 0000000..fcc26db
--- /dev/null
+++ b/Backend/mapping/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'mapping'
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/Application.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/Application.java
new file mode 100644
index 0000000..62b1164
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/Application.java
@@ -0,0 +1,12 @@
+package com.mhp.coding.challenges.mapping;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/controllers/ArticleController.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/controllers/ArticleController.java
new file mode 100644
index 0000000..e533142
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/controllers/ArticleController.java
@@ -0,0 +1,35 @@
+package com.mhp.coding.challenges.mapping.controllers;
+
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto;
+import com.mhp.coding.challenges.mapping.services.ArticleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/article")
+public class ArticleController {
+
+ private final ArticleService articleService;
+
+ @Autowired
+ public ArticleController(ArticleService articleService) {
+ this.articleService = articleService;
+ }
+
+ @GetMapping()
+ public List list() {
+ return articleService.list();
+ }
+
+ @GetMapping("/{id}")
+ public ArticleDto details(@PathVariable Long id) {
+ return articleService.articleForId(id);
+ }
+
+ @PostMapping()
+ public ArticleDto create(@RequestBody ArticleDto articleDto) {
+ return articleService.create(articleDto);
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.java
new file mode 100644
index 0000000..68c1a39
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/mappers/ArticleMapper.java
@@ -0,0 +1,19 @@
+package com.mhp.coding.challenges.mapping.mappers;
+
+import com.mhp.coding.challenges.mapping.models.db.Article;
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ArticleMapper {
+
+ public ArticleDto map(Article article){
+ //TODO
+ return new ArticleDto();
+ }
+
+ public Article map(ArticleDto articleDto) {
+ // Nicht Teil dieser Challenge.
+ return new Article();
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Article.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Article.java
new file mode 100644
index 0000000..5e1de16
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Article.java
@@ -0,0 +1,48 @@
+package com.mhp.coding.challenges.mapping.models.db;
+
+import com.mhp.coding.challenges.mapping.models.db.blocks.ArticleBlock;
+
+import java.util.Set;
+
+public class Article extends DBEntity {
+
+ private String title;
+
+ private String description;
+
+ private String author;
+
+ private Set blocks;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Set getBlocks() {
+ return blocks;
+ }
+
+ public void setBlocks(Set blocks) {
+ this.blocks = blocks;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/DBEntity.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/DBEntity.java
new file mode 100644
index 0000000..d1432d0
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/DBEntity.java
@@ -0,0 +1,36 @@
+package com.mhp.coding.challenges.mapping.models.db;
+
+import java.util.Date;
+
+public class DBEntity {
+
+ private Long id;
+
+ private Date lastModified;
+
+ private String lastModifiedBy;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(Date lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ public String getLastModifiedBy() {
+ return lastModifiedBy;
+ }
+
+ public void setLastModifiedBy(String lastModifiedBy) {
+ this.lastModifiedBy = lastModifiedBy;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Image.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Image.java
new file mode 100644
index 0000000..2a35620
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/Image.java
@@ -0,0 +1,24 @@
+package com.mhp.coding.challenges.mapping.models.db;
+
+public class Image extends DBEntity {
+
+ private String url;
+
+ private ImageSize imageSize;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public ImageSize getImageSize() {
+ return imageSize;
+ }
+
+ public void setImageSize(ImageSize imageSize) {
+ this.imageSize = imageSize;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/ImageSize.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/ImageSize.java
new file mode 100644
index 0000000..b56f606
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/ImageSize.java
@@ -0,0 +1,7 @@
+package com.mhp.coding.challenges.mapping.models.db;
+
+public enum ImageSize {
+ SMALL,
+ MEDIUM,
+ LARGE
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.java
new file mode 100644
index 0000000..9712672
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ArticleBlock.java
@@ -0,0 +1,14 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+public abstract class ArticleBlock {
+
+ private int sortIndex;
+
+ public int getSortIndex() {
+ return sortIndex;
+ }
+
+ public void setSortIndex(int sortIndex) {
+ this.sortIndex = sortIndex;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.java
new file mode 100644
index 0000000..923e0f8
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/GalleryBlock.java
@@ -0,0 +1,18 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+import com.mhp.coding.challenges.mapping.models.db.Image;
+
+import java.util.List;
+
+public class GalleryBlock extends ArticleBlock {
+
+ private List images;
+
+ public List getImages() {
+ return images;
+ }
+
+ public void setImages(List images) {
+ this.images = images;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.java
new file mode 100644
index 0000000..238c804
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/ImageBlock.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+import com.mhp.coding.challenges.mapping.models.db.Image;
+
+public class ImageBlock extends ArticleBlock {
+
+ private Image image;
+
+ public Image getImage() {
+ return image;
+ }
+
+ public void setImage(Image image) {
+ this.image = image;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.java
new file mode 100644
index 0000000..3553a87
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/TextBlock.java
@@ -0,0 +1,14 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+public class TextBlock extends ArticleBlock {
+
+ private String text;
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.java
new file mode 100644
index 0000000..7fb67db
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlock.java
@@ -0,0 +1,24 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+public class VideoBlock extends ArticleBlock {
+
+ private String url;
+
+ private VideoBlockType type;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public VideoBlockType getType() {
+ return type;
+ }
+
+ public void setType(VideoBlockType type) {
+ this.type = type;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlockType.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlockType.java
new file mode 100644
index 0000000..c1bbd45
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/db/blocks/VideoBlockType.java
@@ -0,0 +1,6 @@
+package com.mhp.coding.challenges.mapping.models.db.blocks;
+
+public enum VideoBlockType {
+ YOUTUBE,
+ VIMEO
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.java
new file mode 100644
index 0000000..d03388f
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ArticleDto.java
@@ -0,0 +1,59 @@
+package com.mhp.coding.challenges.mapping.models.dto;
+
+
+import com.mhp.coding.challenges.mapping.models.dto.blocks.ArticleBlockDto;
+
+import java.util.Collection;
+
+public class ArticleDto {
+
+ private Long id;
+
+ private String title;
+
+ private String description;
+
+ private String author;
+
+ private Collection blocks;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Collection getBlocks() {
+ return blocks;
+ }
+
+ public void setBlocks(Collection blocks) {
+ this.blocks = blocks;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ImageDto.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ImageDto.java
new file mode 100644
index 0000000..85aa718
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/ImageDto.java
@@ -0,0 +1,36 @@
+package com.mhp.coding.challenges.mapping.models.dto;
+
+import com.mhp.coding.challenges.mapping.models.db.ImageSize;
+
+public class ImageDto {
+
+ private Long id;
+
+ private String url;
+
+ private ImageSize imageSize;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public ImageSize getImageSize() {
+ return imageSize;
+ }
+
+ public void setImageSize(ImageSize imageSize) {
+ this.imageSize = imageSize;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.java
new file mode 100644
index 0000000..7bb3689
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ArticleBlockDto.java
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks;
+
+public class ArticleBlockDto {
+ private int sortIndex;
+
+ public int getSortIndex() {
+ return sortIndex;
+ }
+
+ public void setSortIndex(int sortIndex) {
+ this.sortIndex = sortIndex;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.java
new file mode 100644
index 0000000..54ecb62
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/GalleryBlockDto.java
@@ -0,0 +1,18 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks;
+
+import com.mhp.coding.challenges.mapping.models.dto.ImageDto;
+
+import java.util.List;
+
+public class GalleryBlockDto extends ArticleBlockDto {
+
+ private List images;
+
+ public List getImages() {
+ return images;
+ }
+
+ public void setImages(List images) {
+ this.images = images;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.java
new file mode 100644
index 0000000..caaceac
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/ImageBlock.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks;
+
+import com.mhp.coding.challenges.mapping.models.dto.ImageDto;
+
+public class ImageBlock extends ArticleBlockDto {
+
+ private ImageDto image;
+
+ public ImageDto getImage() {
+ return image;
+ }
+
+ public void setImage(ImageDto image) {
+ this.image = image;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.java
new file mode 100644
index 0000000..23d0c32
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/TextBlock.java
@@ -0,0 +1,14 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks;
+
+public class TextBlock extends ArticleBlockDto {
+
+ private String text;
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.java
new file mode 100644
index 0000000..d00d3db
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/models/dto/blocks/VideoBlock.java
@@ -0,0 +1,26 @@
+package com.mhp.coding.challenges.mapping.models.dto.blocks;
+
+import com.mhp.coding.challenges.mapping.models.db.blocks.VideoBlockType;
+
+public class VideoBlock extends ArticleBlockDto {
+
+ private String url;
+
+ private VideoBlockType type;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public VideoBlockType getType() {
+ return type;
+ }
+
+ public void setType(VideoBlockType type) {
+ this.type = type;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.java
new file mode 100644
index 0000000..f1e2cdb
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/repositories/ArticleRepository.java
@@ -0,0 +1,96 @@
+package com.mhp.coding.challenges.mapping.repositories;
+
+import com.mhp.coding.challenges.mapping.models.db.Article;
+import com.mhp.coding.challenges.mapping.models.db.Image;
+import com.mhp.coding.challenges.mapping.models.db.ImageSize;
+import com.mhp.coding.challenges.mapping.models.db.blocks.*;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+
+@Component
+public class ArticleRepository {
+
+ public List all(){
+ final List result = new ArrayList<>();
+ result.add(createDummyArticle(1001L));
+ result.add(createDummyArticle(2002L));
+ result.add(createDummyArticle(3003L));
+ result.add(createDummyArticle(4004L));
+ result.add(createDummyArticle(5005L));
+ return result;
+ }
+
+ public Article findBy(Long id){
+ return createDummyArticle(id);
+ }
+
+ public void create(Article article){
+ //Ignore
+ }
+
+ private Article createDummyArticle(Long id) {
+ final Article result = new Article();
+ result.setId(id);
+ result.setAuthor("Max Mustermann");
+ result.setDescription("Article Description " + id);
+ result.setTitle("Article Nr.: " + id);
+ result.setLastModifiedBy("Hans Müller");
+ result.setLastModified(new Date());
+ result.setBlocks(createBlocks(id));
+ return result;
+ }
+
+ private Set createBlocks(Long articleId){
+ final Set result = new HashSet<>();
+
+ final TextBlock textBlock = new TextBlock();
+ textBlock.setText("Some Text for " + articleId);
+ textBlock.setSortIndex(0);
+ result.add(textBlock);
+
+ final ImageBlock imageBlock = new ImageBlock();
+ imageBlock.setImage(createImage(1L));
+ textBlock.setSortIndex(1);
+ result.add(imageBlock);
+
+ final TextBlock secondTextBlock = new TextBlock();
+ secondTextBlock.setText("Second Text for " + articleId);
+ secondTextBlock.setSortIndex(2);
+ result.add(secondTextBlock);
+
+ final GalleryBlock galleryBlock = new GalleryBlock();
+ secondTextBlock.setSortIndex(3);
+
+ final List galleryImages = new ArrayList<>();
+ galleryImages.add(createImage(2L));
+ galleryImages.add(createImage(3L));
+ galleryBlock.setImages(galleryImages);
+
+ result.add(galleryBlock);
+
+ final TextBlock thirdTextBlock = new TextBlock();
+ thirdTextBlock.setText("Third Text for " + articleId);
+ thirdTextBlock.setSortIndex(4);
+ result.add(thirdTextBlock);
+
+ final VideoBlock videoBlock = new VideoBlock();
+ videoBlock.setType(VideoBlockType.YOUTUBE);
+ videoBlock.setUrl("https://youtu.be/myvideo");
+ videoBlock.setSortIndex(5);
+
+ result.add(videoBlock);
+
+ return result;
+ }
+
+ private Image createImage(Long imageId){
+ final Image result = new Image();
+ result.setId(imageId);
+ result.setLastModified(new Date());
+ result.setLastModifiedBy("Max Mustermann");
+ result.setImageSize(ImageSize.LARGE);
+ result.setUrl("https://someurl.com/image/" + imageId);
+ return null;
+ }
+}
diff --git a/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/services/ArticleService.java b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/services/ArticleService.java
new file mode 100644
index 0000000..be87dc2
--- /dev/null
+++ b/Backend/mapping/src/main/java/com/mhp/coding/challenges/mapping/services/ArticleService.java
@@ -0,0 +1,43 @@
+package com.mhp.coding.challenges.mapping.services;
+
+import com.mhp.coding.challenges.mapping.mappers.ArticleMapper;
+import com.mhp.coding.challenges.mapping.models.db.Article;
+import com.mhp.coding.challenges.mapping.models.dto.ArticleDto;
+import com.mhp.coding.challenges.mapping.repositories.ArticleRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class ArticleService {
+
+ private final ArticleRepository repository;
+
+ private final ArticleMapper mapper;
+
+ @Autowired
+ public ArticleService(ArticleRepository repository, ArticleMapper mapper) {
+ this.repository = repository;
+ this.mapper = mapper;
+ }
+
+ public List list() {
+ final List articles = repository.all();
+ //TODO
+ return new ArrayList<>();
+ }
+
+ public ArticleDto articleForId(Long id) {
+ final Article article = repository.findBy(id);
+ //TODO
+ return new ArticleDto();
+ }
+
+ public ArticleDto create(ArticleDto articleDto) {
+ final Article create = mapper.map(articleDto);
+ repository.create(create);
+ return mapper.map(create);
+ }
+}
diff --git a/Backend/mapping/src/main/resources/application.properties b/Backend/mapping/src/main/resources/application.properties
new file mode 100644
index 0000000..e69de29
diff --git a/Backend/mapping/src/test/java/com/mhp/coding/challenges/mapping/ApplicationTests.java b/Backend/mapping/src/test/java/com/mhp/coding/challenges/mapping/ApplicationTests.java
new file mode 100644
index 0000000..433e92e
--- /dev/null
+++ b/Backend/mapping/src/test/java/com/mhp/coding/challenges/mapping/ApplicationTests.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.mapping;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/Backend/retry/.gitignore b/Backend/retry/.gitignore
new file mode 100644
index 0000000..a2a3040
--- /dev/null
+++ b/Backend/retry/.gitignore
@@ -0,0 +1,31 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
diff --git a/Backend/retry/.mvn/wrapper/MavenWrapperDownloader.java b/Backend/retry/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/Backend/retry/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/Backend/retry/.mvn/wrapper/maven-wrapper.jar b/Backend/retry/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..2cc7d4a
Binary files /dev/null and b/Backend/retry/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/Backend/retry/.mvn/wrapper/maven-wrapper.properties b/Backend/retry/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..642d572
--- /dev/null
+++ b/Backend/retry/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/Backend/retry/README.md b/Backend/retry/README.md
new file mode 100644
index 0000000..aa39752
--- /dev/null
+++ b/Backend/retry/README.md
@@ -0,0 +1,71 @@
+# Backend Coding Challenge: E-Mail Retry
+
+## Description:
+
+This Spring-Boot-Project consists the relevant parts of a microservice (Notification Service), that is responsible for sending notifications via different communication channels (E-Mail, SMS, Push Notification). The `EmailController` provides an HTTP interface, that can be used to send new emails.
+
+In the current increment the transmission of emails is already performed asynchronously. Therefore the user of the interface receives an immediate `200 OK` status, in response to the HTTP request. The email is then *eventually* transmitted to the recipient. If an error occurs during the email delivery (e.g. because of a fault of the SMTP server) the user will **not** be notified of the failure and the notification will **not** be delivered.
+
+During the past weeks the Support Team received a large amount of complaints, stating that emails could not be delivered to the recipients. The cause was traced back to temporary disorders of the SMTP Server.
+
+Being the new developer in the Notification Team, you receive the task to implement a retry mechanism for the transmission of emails. This should ensure that the messages are being successfully delivered to the recipients in the future, even during disorders of the SMTP Server.
+
+## Acceptance Criteria:
+
+1. Per email notification there must be exactly 5 retry attempts. The first retry has to be made after 5 seconds, subsequent retrys must be executed in exponential time-displacement.
+2. If the Notification Service crashes or is restarting during active retry processes, the retrys must continue with the state they were in before the reboot (e.g. 2 retrys successfully executed → Crash Notification Service → Restart Notification Service → Execute the remaining 3 retrys).
+3. The implemented retry mechanism has to be developed for a multi-instance setup of the Notification Service. Please be aware of the fact during your architectural decisions.
+
+## General Conditions:
+
+- In the root directory of the project a `docker-compose.yml` can be found. By executing `docker-compose up` the required infrastructure for the challenge can be started in docker containers.
+The following services are being started:
+ - MongoDB (NoSQL Database): `[http://localhost:27017](http://localhost:27017)`
+ - Mailhog (Mail Server Mock): `[http://localhost:1025](http://localhost:1025)` (SMTP Server), `[http://localhost:8025](http://localhost:8025)` (Web UI - Inbox)
+
+ For successful completion of the challenge only the provided services may be used. You are not forced to use all of them, but it is not allowed to use additional services.
+
+- The structure of the microservice at hand follows the Hexagonal-Architecture (aka. Clean Architecture - [https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)). Incorporate your implementation into the existing application architecture.
+- By default the mail server mock (MailHog) is configured to reject 50% of the received emails. This behaviour can be modified or disabled in the `docker-compose.yml`.
+- You should complete this coding challenge within `3` days. Please keep this guidance in mind and submit your (partial) result upon expiration of this time frame.
+- If you draw up any notes or sketches during your implementation, please hand them in together with your solution.
+
+## Bonus Tasks:
+
+1. Write unit- and integration tests for your implemented retry mechanism.
+
+---
+
+## Beschreibung:
+
+Dieses Spring-Boot-Projekt beinhaltet den relevanten Ausschnitt eines Microservices (Notification Service), der für den Versand von Benachrichtigungen über verschiedene Kommunikationskanäle (E-Mail, SMS, Push Notification) zuständig ist. Der `EmailController` stellt eine HTTP Schnittstelle zur Verfügung, über die sich eine neue Benachrichtigung per E-Mail versenden lässt.
+
+In der aktuellen Ausbaustufe wird der E-Mail Versand bereits asynchron ausgeführt. Als Antwort auf den HTTP Request erhält der Benutzer der Schnittstelle somit unmittelbar einen Status `200 OK` und die E-Mail wird dem Empfänger daraufhin *irgendwann* zugestellt. Tritt während der Zustellung ein Fehler auf (z. B. der SMTP Server hat eine Störung und antwortet nicht) wird der Benutzer **nicht** informiert und die Benachrichtigung wird **nicht** übermittelt.
+
+In den letzten Wochen sind vermehrt Beschwerden über nicht zugestellt E-Mails beim Support Team eingegangen. Die Ursache ließ sich auf temporäre Ausfälle des SMTP Servers zurückführen.
+
+Als neuer Entwickler im Notification Team erhältst du deshalb die Aufgabe einen Retry-Mechanismus für den E-Mail Versand zu implementieren. So soll sichergestellt werden, dass die Nachrichten zukünftig auch im Falle einer kurzzeitigen Störung des SMTP Servers erfolgreich an den Empfänger übermittelt werden.
+
+## Akzeptanzkriterien
+
+1. Pro E-Mail Benachrichtigung muss es genau 5 Versuche für die Zustellung geben. Der erste Versuch muss nach 5 Sekunden erfolgen und darauffolgende Retrys müssen in exponentiellem Abstand voneinander ausgeführt werden.
+2. Startet der Notification Service neu, während einem oder mehreren aktiven Retry-Prozessen, müssen die Retrys nach dem Neustart der Anwendung an der Stelle fortgesetzt werden an der diese unterbrochen wurden (z. B.: 2 Retrys erfolgreich ausgeführt → Crash Notification Service → Neustart Notification Service → Ausführung der verbleibenden 3 Retrys).
+3. Der implementierte Retry-Mechanismus muss für ein hochverfügbares Setup des Notification Service (d. h. mit mehreren parallel ausgeführten Instanzen) entwickelt werden. Beachte diesen Umstand bei deinen Architekturentscheidungen.
+
+## Rahmenbedingungen:
+
+- Im Hauptverzeichnis des Projekts liegt eine `docker-compose.yml`. Mit dem Befehl `docker-compose up` lässt sich die benötigte Infrastruktur für die Challenge in Docker starten.
+Dabei werden folgende Dienste bereitgestellt:
+ - MongoDB (NoSQL Datenbank): `[http://localhost:27017](http://localhost:27017)`
+ - Mailhog (Mail Server Mock): `[http://localhost:1025](http://localhost:1025)` (SMTP Server), `[http://localhost:8025](http://localhost:8025)` (Web UI - Inbox)
+
+ Zur erfolgreichen Fertigstellung der Challenge dürfen ausschließlich die aufgelisteten Dienste verwendet werden. Die Verwendung dieser Dienste ist nicht verpflichtend, aber es dürfen keine zusätzlichen Anwendungen eingesetzt werden.
+
+- Die Struktur des vorliegenden Microservices folgt der Hexagonalen-Architektur (aka. Clean Architecture - [https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)). Halte dich bei deiner Implementierung an die bestehende Anwendungsarchitektur.
+- Standardmäßig ist der Mail Server Mock (Mailhog) so konfiguriert, dass nur 50% der E-Mails erfolgreich verarbeitet werden. Dieses Verhalten lässt sich über die `docker-compose.yml` anpassen oder komplett deaktivieren.
+- Für diese Coding Challenge haben wir `3` Tage eingeplant. Bitte richte dich bei deiner Umsetzung nach diesem Richtwert und reiche dein (Teil-)Ergebnis nach Ablauf dieser Arbeitszeit ein.
+- Solltest du während der Bearbeitung Zeichnungen oder Notizen anfertigen, reiche diese zusammen mit deiner Lösungen ein.
+
+## Bonus Aufgaben:
+
+1. Schreibe Unit- und Integrationstests für deinen implementierten Retry-Mechanismus.
diff --git a/Backend/retry/docker-compose.yml b/Backend/retry/docker-compose.yml
new file mode 100644
index 0000000..71dc8f4
--- /dev/null
+++ b/Backend/retry/docker-compose.yml
@@ -0,0 +1,18 @@
+version: '3'
+
+services:
+ mongodb:
+ container_name: mhp-mongodb
+ image: mongo
+ restart: always
+ ports:
+ - 27017:27017
+
+ mailhog:
+ container_name: mhp-mailhog
+ image: mailhog/mailhog:latest
+ restart: always
+ command: -invite-jim -jim-accept=0.5 -jim-disconnect=0 -jim-linkspeed-affect=0 -jim-reject-sender=0 -jim-reject-recipient=0 -jim-reject-auth=0
+ ports:
+ - 1025:1025
+ - 8025:8025
diff --git a/Backend/retry/mvnw b/Backend/retry/mvnw
new file mode 100755
index 0000000..a16b543
--- /dev/null
+++ b/Backend/retry/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/Backend/retry/mvnw.cmd b/Backend/retry/mvnw.cmd
new file mode 100644
index 0000000..c8d4337
--- /dev/null
+++ b/Backend/retry/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/Backend/retry/pom.xml b/Backend/retry/pom.xml
new file mode 100644
index 0000000..3a0718c
--- /dev/null
+++ b/Backend/retry/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.4.RELEASE
+
+
+ com.mhp.coding.challenges
+ retry
+ 0.0.1-SNAPSHOT
+ retry
+ Demo project for Spring Boot
+
+
+ 11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.10
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/RetryApplication.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/RetryApplication.java
new file mode 100644
index 0000000..48bacda
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/RetryApplication.java
@@ -0,0 +1,15 @@
+package com.mhp.coding.challenges.retry;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@SpringBootApplication
+@EnableAsync
+public class RetryApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(RetryApplication.class, args);
+ }
+
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/configuration/GlobalBeanConfiguration.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/configuration/GlobalBeanConfiguration.java
new file mode 100644
index 0000000..e70b763
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/configuration/GlobalBeanConfiguration.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.retry.configuration;
+
+import com.mhp.coding.challenges.retry.core.inbound.NotificationHandler;
+import com.mhp.coding.challenges.retry.core.logic.NotificationService;
+import com.mhp.coding.challenges.retry.core.outbound.NotificationSender;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class GlobalBeanConfiguration {
+
+ @Bean
+ public NotificationHandler notificationHandler(NotificationSender notificationSender) {
+ return new NotificationService(notificationSender);
+ }
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/entities/EmailNotification.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/entities/EmailNotification.java
new file mode 100644
index 0000000..41ba0c2
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/entities/EmailNotification.java
@@ -0,0 +1,18 @@
+package com.mhp.coding.challenges.retry.core.entities;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class EmailNotification {
+
+ @NotBlank
+ private String recipient;
+
+ @NotBlank
+ private String subject;
+
+ @NotBlank
+ private String text;
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/inbound/NotificationHandler.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/inbound/NotificationHandler.java
new file mode 100644
index 0000000..1556cf8
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/inbound/NotificationHandler.java
@@ -0,0 +1,8 @@
+package com.mhp.coding.challenges.retry.core.inbound;
+
+import com.mhp.coding.challenges.retry.core.entities.EmailNotification;
+
+public interface NotificationHandler {
+
+ EmailNotification processEmailNotification(EmailNotification emailNotification);
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/logic/NotificationService.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/logic/NotificationService.java
new file mode 100644
index 0000000..0bad44a
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/logic/NotificationService.java
@@ -0,0 +1,20 @@
+package com.mhp.coding.challenges.retry.core.logic;
+
+import com.mhp.coding.challenges.retry.core.entities.EmailNotification;
+import com.mhp.coding.challenges.retry.core.inbound.NotificationHandler;
+import com.mhp.coding.challenges.retry.core.outbound.NotificationSender;
+
+public class NotificationService implements NotificationHandler {
+
+ private NotificationSender notificationSender;
+
+ public NotificationService(NotificationSender notificationSender) {
+ this.notificationSender = notificationSender;
+ }
+
+ @Override
+ public EmailNotification processEmailNotification(EmailNotification emailNotification) {
+ notificationSender.sendEmail(emailNotification);
+ return emailNotification;
+ }
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/outbound/NotificationSender.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/outbound/NotificationSender.java
new file mode 100644
index 0000000..1282371
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/core/outbound/NotificationSender.java
@@ -0,0 +1,11 @@
+package com.mhp.coding.challenges.retry.core.outbound;
+
+import com.mhp.coding.challenges.retry.core.entities.EmailNotification;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+public interface NotificationSender {
+
+ void sendEmail(@Valid @NotNull EmailNotification emailNotification);
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/inbound/EmailController.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/inbound/EmailController.java
new file mode 100644
index 0000000..8cb8927
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/inbound/EmailController.java
@@ -0,0 +1,26 @@
+package com.mhp.coding.challenges.retry.inbound;
+
+import com.mhp.coding.challenges.retry.core.entities.EmailNotification;
+import com.mhp.coding.challenges.retry.core.inbound.NotificationHandler;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/v1/emails")
+public class EmailController {
+
+ private NotificationHandler notificationHandler;
+
+ public EmailController(NotificationHandler notificationHandler) {
+ this.notificationHandler = notificationHandler;
+ }
+
+ @PostMapping
+ public ResponseEntity createEmailNotification(@RequestBody EmailNotification emailNotification) {
+ EmailNotification emailNotificationResult = notificationHandler.processEmailNotification(emailNotification);
+ return ResponseEntity.ok(emailNotificationResult);
+ }
+}
diff --git a/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/outbound/EmailNotificationSenderService.java b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/outbound/EmailNotificationSenderService.java
new file mode 100644
index 0000000..b453890
--- /dev/null
+++ b/Backend/retry/src/main/java/com/mhp/coding/challenges/retry/outbound/EmailNotificationSenderService.java
@@ -0,0 +1,41 @@
+package com.mhp.coding.challenges.retry.outbound;
+
+import com.mhp.coding.challenges.retry.core.entities.EmailNotification;
+import com.mhp.coding.challenges.retry.core.outbound.NotificationSender;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+@Service
+@Validated
+public class EmailNotificationSenderService implements NotificationSender {
+
+ private static final String SENDER_ADDRESS = "info@mhp.com";
+
+ private JavaMailSender mailSender;
+
+ public EmailNotificationSenderService(JavaMailSender mailSender) {
+ this.mailSender = mailSender;
+ }
+
+ @Async
+ @Override
+ public void sendEmail(@Valid @NotNull EmailNotification emailNotification) {
+ try {
+ SimpleMailMessage mailMessage = new SimpleMailMessage();
+ mailMessage.setFrom(SENDER_ADDRESS);
+ mailMessage.setTo(emailNotification.getRecipient());
+ mailMessage.setSubject(emailNotification.getSubject());
+ mailMessage.setText(emailNotification.getText());
+
+ mailSender.send(mailMessage);
+ } catch (Exception e) {
+ throw new RuntimeException(String.format("Failed to send email to recipient: %s", emailNotification.getRecipient()));
+ }
+ }
+}
diff --git a/Backend/retry/src/main/resources/application.yml b/Backend/retry/src/main/resources/application.yml
new file mode 100644
index 0000000..c415549
--- /dev/null
+++ b/Backend/retry/src/main/resources/application.yml
@@ -0,0 +1,13 @@
+spring:
+ mail:
+ host: localhost
+ properties:
+ mail:
+ transport:
+ protocol: smtp
+ smtp:
+ port: 1025
+ auth: false
+ starttls:
+ enable: false
+ required: false
diff --git a/Backend/retry/src/test/java/com/mhp/coding/challenges/retry/RetryApplicationTests.java b/Backend/retry/src/test/java/com/mhp/coding/challenges/retry/RetryApplicationTests.java
new file mode 100644
index 0000000..1c82595
--- /dev/null
+++ b/Backend/retry/src/test/java/com/mhp/coding/challenges/retry/RetryApplicationTests.java
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.retry;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class RetryApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/Backend/security/README.md b/Backend/security/README.md
new file mode 100644
index 0000000..776f97e
--- /dev/null
+++ b/Backend/security/README.md
@@ -0,0 +1,73 @@
+# Backend Coding Challenge: Authentication
+
+## Description:
+
+Within the framework of an agile project, new security mechanisms for an access system of doors are to be implemented. Until now, access to the system via network zones and VPN has been secured on an on-premises hosted system, thus, a security implementation was not mandatory until now. However, in the course of a planned migration to the cloud, this requirement has changed.
+
+Access to the current version of the service is available to anyone who has access to the network. This access lets you access all doors and even unlock them. The service is accessible externally through a REST API and can be read and modified through this. This represents a security risk and must not be ignored.
+
+Your first project as a new developer in the company will be to secure both endpoints of the service.
+
+## Acceptance Criteria:
+
+### (Junior) Consultant:
+
+1. Both endpoints need to be secured with OAuth2
+2. OAuth2 can be implemented with either Spring or Keycloak
+3. OAuth2 should be implemented with a local authorization server
+
+### Senior Consultant and above:
+
+1. Both endpoints need to be secured with MTLS
+2. MTLS can be implemented with either Spring, nginx or apache
+3. Used certificates can either be self signed or valid ones for example from Let's encrypt
+4. The backend/rest controller should validate the sent certificates
+
+## General Conditions:
+- The structure of the microservice at hand follows the Hexagonal-Architecture (aka. Clean Architecture - [https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)). Incorporate your implementation into the existing application architecture
+- Additional dependencies can be added if needed, as well as external software like docker
+- The included tests will only be successful when both endpoints return an HTTP 403
+- You should complete this coding challenge within `3` days. Please keep this guidance in mind and submit your (partial) result upon expiration of this time frame
+- If you draw up any notes or sketches during your implementation, please hand them in together with your solution
+
+## Bonus Tasks (everyone):
+
+- Write additional integration tests for your security implementation
+- Secure both API endpoints with different credentials/roles/certificates
+
+---
+
+## Beschreibung:
+
+Im Rahmen eines agilen Projekts sollen neue Sicherheitsmechanismen für ein Zugangssystem von Türen implementiert werden. Bisher wurde der Zugriff zum System über Netzwerkzonen und VPN auf ein On-Premises gehostetes System abgesichert. Daher war eine Sicherheitsimplementierung bisher nicht zwingend notwendig. Im Zuge einer geplanten Migration in die Cloud, hat sich diese Anforderung jedoch geändert.
+
+Zugriff auf die aktuelle Version des Services hat jeder, der Zugang zum Netzwerk hat. Dieser Zugang lässt einen sämtliche Türen abrufen und diese sogar entriegeln. Der Service ist nach außen durch eine REST API erreichbar und kann über diese ausgelesen und verändert werden. Dies stellt ein Sicherheitsrisiko dar und darf nicht ignoriert werden.
+
+Dein erstes Projekt als neuer Entwickler im Unternehmen wird es sein, beide Endpunkte des Services abzusichern.
+
+## Akzeptanzkriterien:
+
+### (Junior) Consultant:
+
+1. Beide API Endpunkte sollen durch OAuth2 gesichert weren
+2. OAuth2 kann entweder mit Spring oder Keycloak realisiert werden
+3. OAuth2 soll mit einem lokalen Autorisierungsserver realisiert werden
+
+### Senior Consultant und höher:
+
+1. Beide API Endpunkte sollen durch MTLS gesichert weren
+2. MTLs kann mit Spring, nginx oder apache realisiert werden
+3. Die verwendeten Zertifikate können selbst signiert oder valide sein (z.B. von let's encrypt)
+4. Das Backend/Rest Controller soll die gesendeten Zertifikate auf Validität prüfen
+
+## Rahmenbedingungen:
+- Die Struktur des vorliegenden Microservices folgt der Hexagonalen-Architektur (aka. Clean Architecture - [https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)). Halte dich bei deiner Implementierung an die bestehende Anwendungsarchitektur
+- Zusätzlich Abhängigkeiten dürfen hinzugefügt werden, dies gilt auch für externe Software wie docker
+- Die mitgelieferten Tests sind nur dann erfolgreich, wenn beide Endpunkte einen HTTP 403 zurückgeben.
+- Für diese Coding Challenge haben wir `3` Tage eingeplant. Bitte richte dich bei deiner Umsetzung nach diesem Richtwert und reiche dein (Teil-)Ergebnis nach Ablauf dieser Arbeitszeit ein
+- Solltest du während der Bearbeitung Zeichnungen oder Notizen anfertigen, reiche diese zusammen mit deiner Lösungen ein
+
+## Bonusaufgaben (alle):
+
+- Schreibe zusätzlich Integration Tests für deine Sicherheitsimplementierung
+- Sichere beide API Endpunkte durch unterschiedliche Zugangsdaten/Rollen/Zertifikate
diff --git a/Backend/security/mvnw b/Backend/security/mvnw
new file mode 100644
index 0000000..a16b543
--- /dev/null
+++ b/Backend/security/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/Backend/security/mvnw.cmd b/Backend/security/mvnw.cmd
new file mode 100644
index 0000000..c8d4337
--- /dev/null
+++ b/Backend/security/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/Backend/security/pom.xml b/Backend/security/pom.xml
new file mode 100644
index 0000000..db84823
--- /dev/null
+++ b/Backend/security/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.0.RELEASE
+
+
+ com.mhp.coding.challenges
+ auth
+ 0.0.1-SNAPSHOT
+ auth
+ Authentication Examples
+
+
+ 11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/AuthApplication.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/AuthApplication.java
new file mode 100644
index 0000000..dcdd7dc
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/AuthApplication.java
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.auth;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AuthApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AuthApplication.class, args);
+ }
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/configuration/GlobalBeanConfiguration.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/configuration/GlobalBeanConfiguration.java
new file mode 100644
index 0000000..cae96c8
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/configuration/GlobalBeanConfiguration.java
@@ -0,0 +1,16 @@
+package com.mhp.coding.challenges.auth.configuration;
+
+import com.mhp.coding.challenges.auth.core.logic.DoorService;
+import com.mhp.coding.challenges.auth.core.outbound.DoorDatabaseProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class GlobalBeanConfiguration {
+
+ @Bean(name = "doorService")
+ public DoorService doorService(DoorDatabaseProvider doorDatabaseProvider) {
+ return new DoorService(doorDatabaseProvider);
+ }
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/Door.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/Door.java
new file mode 100644
index 0000000..5a1f844
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/Door.java
@@ -0,0 +1,35 @@
+package com.mhp.coding.challenges.auth.core.entities;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Door {
+
+ private long id;
+ private String type;
+ private String location;
+ private State state;
+
+ public Door(long id, String type, String location, State state) {
+ this.id = id;
+ this.type = type;
+ this.location = location;
+ this.state = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (!(o instanceof Door)) return false;
+ Door d = (Door) o;
+ return d.getId() == this.id
+ && d.getType() != null
+ && d.getType().equals(this.getType())
+ && d.getLocation() != null
+ && d.getLocation().equals(this.location)
+ && d.getState() == this.state;
+ }
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/State.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/State.java
new file mode 100644
index 0000000..78b1257
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/entities/State.java
@@ -0,0 +1,6 @@
+package com.mhp.coding.challenges.auth.core.entities;
+
+public enum State {
+ LOCKED,
+ UNLOCKED,
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/inbound/DoorProvider.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/inbound/DoorProvider.java
new file mode 100644
index 0000000..8066d6e
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/inbound/DoorProvider.java
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.auth.core.inbound;
+
+import com.mhp.coding.challenges.auth.core.entities.Door;
+
+import java.util.List;
+
+public interface DoorProvider {
+
+ List triggerDoorListing();
+
+ Door triggerDoorStateChange(Door door);
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/logic/DoorService.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/logic/DoorService.java
new file mode 100644
index 0000000..75354c5
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/logic/DoorService.java
@@ -0,0 +1,28 @@
+package com.mhp.coding.challenges.auth.core.logic;
+
+import com.mhp.coding.challenges.auth.core.entities.Door;
+import com.mhp.coding.challenges.auth.core.inbound.DoorProvider;
+import com.mhp.coding.challenges.auth.core.outbound.DoorDatabaseProvider;
+
+import java.util.List;
+
+public class DoorService implements DoorProvider {
+
+ private final DoorDatabaseProvider doorDatabaseProvider;
+
+ public DoorService(DoorDatabaseProvider doorDatabaseProvider) {
+ this.doorDatabaseProvider = doorDatabaseProvider;
+ }
+
+ @Override
+ public List triggerDoorListing() {
+ return this.doorDatabaseProvider.readDoors();
+ }
+
+ @Override
+ public Door triggerDoorStateChange(Door door) {
+ if (door.getId() <= 0 || door.getState() == null) return null;
+ return this.doorDatabaseProvider.changeDoorState(door);
+ }
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/outbound/DoorDatabaseProvider.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/outbound/DoorDatabaseProvider.java
new file mode 100644
index 0000000..6f58a0f
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/core/outbound/DoorDatabaseProvider.java
@@ -0,0 +1,13 @@
+package com.mhp.coding.challenges.auth.core.outbound;
+
+import com.mhp.coding.challenges.auth.core.entities.Door;
+
+import java.util.List;
+
+public interface DoorDatabaseProvider {
+
+ List readDoors();
+
+ Door changeDoorState(Door door);
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/inbound/DoorController.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/inbound/DoorController.java
new file mode 100644
index 0000000..7fc74f4
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/inbound/DoorController.java
@@ -0,0 +1,32 @@
+package com.mhp.coding.challenges.auth.inbound;
+
+import com.mhp.coding.challenges.auth.core.entities.Door;
+import com.mhp.coding.challenges.auth.core.inbound.DoorProvider;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/v1/door")
+public class DoorController {
+
+ private final DoorProvider doorProvider;
+
+ public DoorController(DoorProvider doorProvider) {
+ this.doorProvider = doorProvider;
+ }
+
+ @GetMapping
+ @ResponseBody
+ public ResponseEntity> listDoors() {
+ return ResponseEntity.ok(this.doorProvider.triggerDoorListing());
+ }
+
+ @PostMapping
+ public ResponseEntity changeDoorState(@RequestBody Door door) {
+ Door newDoor = this.doorProvider.triggerDoorStateChange(door);
+ return newDoor == null ? ResponseEntity.badRequest().build() : ResponseEntity.ok(newDoor);
+ }
+
+}
diff --git a/Backend/security/src/main/java/com/mhp/coding/challenges/auth/outbound/DoorDatabaseService.java b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/outbound/DoorDatabaseService.java
new file mode 100644
index 0000000..6da1723
--- /dev/null
+++ b/Backend/security/src/main/java/com/mhp/coding/challenges/auth/outbound/DoorDatabaseService.java
@@ -0,0 +1,40 @@
+package com.mhp.coding.challenges.auth.outbound;
+
+import com.mhp.coding.challenges.auth.core.entities.Door;
+import com.mhp.coding.challenges.auth.core.entities.State;
+import com.mhp.coding.challenges.auth.core.outbound.DoorDatabaseProvider;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class DoorDatabaseService implements DoorDatabaseProvider {
+
+ private static List doors;
+
+ public DoorDatabaseService() {
+ doors = new ArrayList<>() {
+ {
+ add(new Door(1, "fireproof", "Basement 5.3", State.LOCKED));
+ add(new Door(2, "normal", "Office 3.1.2", State.UNLOCKED));
+ add(new Door(2, "normal", "Office 3.1.3", State.LOCKED));
+ }
+ };
+ }
+
+ @Override
+ public List readDoors() {
+ return doors;
+ }
+
+ @Override
+ public Door changeDoorState(Door door) {
+ Optional doorToChange = doors.stream().filter(d -> d.getId() == door.getId()).findFirst();
+ if (doorToChange.isEmpty()) return null;
+ doorToChange.get().setState(door.getState());
+ return doorToChange.get();
+ }
+
+}
diff --git a/Backend/security/src/main/resources/application.properties b/Backend/security/src/main/resources/application.properties
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/Backend/security/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/Backend/security/src/test/java/com/mhp/coding/challenges/auth/inbound/DoorControllerTest.java b/Backend/security/src/test/java/com/mhp/coding/challenges/auth/inbound/DoorControllerTest.java
new file mode 100644
index 0000000..d9524d0
--- /dev/null
+++ b/Backend/security/src/test/java/com/mhp/coding/challenges/auth/inbound/DoorControllerTest.java
@@ -0,0 +1,46 @@
+package com.mhp.coding.challenges.auth.inbound;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class DoorControllerTest {
+
+ private static final String DOOR_TO_CHANGE = "{\"id\":1,\"state\":\"UNLOCKED\"}";
+
+ private final CloseableHttpClient httpClient = HttpClients.createDefault();
+
+ @Test
+ public void test_get_all_doors() throws Exception {
+ // #Arrange
+ HttpGet request = new HttpGet("http://127.0.0.1:8080/v1/door");
+
+ // #Act
+ CloseableHttpResponse response = httpClient.execute(request);
+
+ // #Assert
+ assertEquals(403, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test_change_door_state() throws Exception {
+ // #Arrange
+ HttpPost request = new HttpPost("http://127.0.0.1:8080/v1/door");
+ request.addHeader("content-type", "application/json");
+ StringEntity jsonEntity = new StringEntity(DOOR_TO_CHANGE);
+ request.setEntity(jsonEntity);
+
+ // #Act
+ CloseableHttpResponse response = httpClient.execute(request);
+
+ // #Assert
+ assertEquals(403, response.getStatusLine().getStatusCode());
+ }
+
+}
diff --git a/README.md b/Full-stack Web/README.md
similarity index 63%
rename from README.md
rename to Full-stack Web/README.md
index b302970..0f28e54 100644
--- a/README.md
+++ b/Full-stack Web/README.md
@@ -1,4 +1,4 @@
-# Fullstack Web-App Coding Challenge 👨🏼💻
+# Full-stack Web-App Coding Challenge 👨🏼💻
## Task
@@ -17,7 +17,7 @@ Use the following free and open API to gather data data:
- [Game of Thrones API](https://anapioficeandfire.com/Documentation#houses)
### Architecture Hint
-
+
### Requirements
@@ -26,8 +26,12 @@ Use the following free and open API to gather data data:
- Backend: Spring-boot, Kotlin
- The Frontend is not allowed to communicate with 3rd party (external api) directly
- The Backend acts as a middleware
-- Use a PRIVATE github repository to provide us your project
+- Use a PRIVATE GitHub repository to provide us your project
+
+### Attach
+
+Please remember to write tests and implement a thorough and detailed documentation of your project to accompany your submission, in order for your application to be considered by our reviewers.
### Hint
-Keep in mind that his is a demonstration of your capabilites. So go ahead and impress us 🤯
+Keep in mind that this is a demonstration of your capabilities. So go ahead and impress us 🤯
\ No newline at end of file
diff --git a/Mobile/README.md b/Mobile/README.md
new file mode 100644
index 0000000..9394119
--- /dev/null
+++ b/Mobile/README.md
@@ -0,0 +1,35 @@
+# Mobile-App Coding Challenge 👨🏼💻
+
+## Challenge
+
+Create a mobile application. The app should show the user all Game of Thrones houses in a list.
+
+It should be possible to select a house from the list. By tapping on a entry the house should appear in a detail view were you should display more information to the selected house.
+
+### APIs and Docs
+
+* [Game Of Thrones API](https://anapioficeandfire.com)
+
+## Requirements
+
+- Build a native app
+ - Android
+ - Code using Kotlin
+ - Min. API Level 21
+ - Avoid use of third party dependencies where possible
+ - Select on of the following UI-Toolkits:
+ - Legacy: use `RecyclerView`
+ - Jetpack Compose
+ - iOS
+ - Code using Swift
+ - Deployment Target iOS 14
+ - No use of third party dependencies
+- Please provide the full git repository when sending in your results
+
+### Attach
+
+Please remember to write tests and implement a thorough and detailed documentation of your project to accompany your submission, in order for your application to be considered by our reviewers.
+
+### Hint
+
+Keep in mind that this is a demonstration of your capabilities. So go ahead and impress us 🤯
\ No newline at end of file