Skip to content

Commit 4cc143c

Browse files
committed
Add OTA demo sample with mock HTTP handler
1 parent b760d71 commit 4cc143c

6 files changed

Lines changed: 506 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="../../LocalizationManager.JsonLocalization/LocalizationManager.JsonLocalization.csproj" />
12+
</ItemGroup>
13+
14+
<!-- Embedded fallback resources (used when OTA is offline) -->
15+
<ItemGroup>
16+
<EmbeddedResource Include="Resources/*.json" WithCulture="false" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright (c) 2025 Nikolaos Protopapas
2+
// Licensed under the MIT License
3+
4+
using System.Net;
5+
using System.Net.Http.Headers;
6+
using System.Security.Cryptography;
7+
using System.Text;
8+
using System.Text.Json;
9+
10+
namespace ConsoleApp.OtaDemo;
11+
12+
/// <summary>
13+
/// HTTP handler that simulates the LRM Cloud OTA API.
14+
/// This allows running OTA demos without a real server connection.
15+
/// </summary>
16+
public class MockOtaHandler : DelegatingHandler
17+
{
18+
private int _version = 1;
19+
private DateTime _versionTimestamp = DateTime.UtcNow;
20+
private readonly Dictionary<string, Dictionary<string, object>> _translations;
21+
22+
/// <summary>
23+
/// When true, simulates network failures (for fallback demo).
24+
/// </summary>
25+
public bool SimulateOffline { get; set; }
26+
27+
/// <summary>
28+
/// Number of requests received (for demo logging).
29+
/// </summary>
30+
public int RequestCount { get; private set; }
31+
32+
public MockOtaHandler() : base(new HttpClientHandler())
33+
{
34+
// Initialize with mock translations that mimic LRM Cloud bundle format
35+
_translations = new Dictionary<string, Dictionary<string, object>>
36+
{
37+
// Note: Plural forms use Dictionary<string, string> to match OtaResourceLoader.ConvertToJson expectations
38+
["en"] = new()
39+
{
40+
["Welcome"] = "Welcome to LRM!",
41+
["Goodbye"] = "Goodbye!",
42+
["AppTitle"] = "OTA Demo Application",
43+
["Greeting"] = "Hello, {0}!",
44+
// Plural forms (CLDR format) - must be Dictionary<string, string>
45+
["Items"] = new Dictionary<string, string>
46+
{
47+
["one"] = "{0} item",
48+
["other"] = "{0} items"
49+
},
50+
["Messages"] = new Dictionary<string, string>
51+
{
52+
["zero"] = "No messages",
53+
["one"] = "{0} message",
54+
["other"] = "{0} messages"
55+
}
56+
},
57+
["fr"] = new()
58+
{
59+
["Welcome"] = "Bienvenue sur LRM!",
60+
["Goodbye"] = "Au revoir!",
61+
["AppTitle"] = "Application Demo OTA",
62+
["Greeting"] = "Bonjour, {0}!",
63+
["Items"] = new Dictionary<string, string>
64+
{
65+
["one"] = "{0} article",
66+
["other"] = "{0} articles"
67+
},
68+
["Messages"] = new Dictionary<string, string>
69+
{
70+
["zero"] = "Aucun message",
71+
["one"] = "{0} message",
72+
["other"] = "{0} messages"
73+
}
74+
},
75+
["de"] = new()
76+
{
77+
["Welcome"] = "Willkommen bei LRM!",
78+
["Goodbye"] = "Auf Wiedersehen!",
79+
["AppTitle"] = "OTA-Demo-Anwendung",
80+
["Greeting"] = "Hallo, {0}!",
81+
["Items"] = new Dictionary<string, string>
82+
{
83+
["one"] = "{0} Artikel",
84+
["other"] = "{0} Artikel"
85+
},
86+
["Messages"] = new Dictionary<string, string>
87+
{
88+
["zero"] = "Keine Nachrichten",
89+
["one"] = "{0} Nachricht",
90+
["other"] = "{0} Nachrichten"
91+
}
92+
}
93+
};
94+
}
95+
96+
/// <summary>
97+
/// Simulates a translation update (as if someone edited via LRM Cloud web UI).
98+
/// </summary>
99+
public void SimulateUpdate(string language, string key, string value)
100+
{
101+
if (!_translations.ContainsKey(language))
102+
{
103+
_translations[language] = new Dictionary<string, object>();
104+
}
105+
_translations[language][key] = value;
106+
_version++;
107+
_versionTimestamp = DateTime.UtcNow;
108+
}
109+
110+
/// <summary>
111+
/// Gets the current version string (for demo display).
112+
/// </summary>
113+
public string CurrentVersion => _versionTimestamp.ToString("O");
114+
115+
protected override Task<HttpResponseMessage> SendAsync(
116+
HttpRequestMessage request,
117+
CancellationToken cancellationToken)
118+
{
119+
RequestCount++;
120+
121+
// Simulate offline mode
122+
if (SimulateOffline)
123+
{
124+
throw new HttpRequestException("Simulated network failure - OTA server unreachable");
125+
}
126+
127+
var uri = request.RequestUri?.ToString() ?? "";
128+
129+
// Handle bundle requests: /api/ota/.../bundle
130+
if (uri.Contains("/bundle"))
131+
{
132+
return Task.FromResult(HandleBundleRequest(request));
133+
}
134+
135+
// Handle version requests: /api/ota/.../version
136+
if (uri.Contains("/version"))
137+
{
138+
return Task.FromResult(HandleVersionRequest());
139+
}
140+
141+
// Unknown endpoint
142+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
143+
}
144+
145+
private HttpResponseMessage HandleBundleRequest(HttpRequestMessage request)
146+
{
147+
// Compute ETag based on version
148+
var etag = ComputeETag(_versionTimestamp.ToString("O"));
149+
150+
// Check If-None-Match for conditional request (304 Not Modified)
151+
var ifNoneMatch = request.Headers.IfNoneMatch.FirstOrDefault()?.Tag;
152+
if (ifNoneMatch == $"\"{etag}\"")
153+
{
154+
return new HttpResponseMessage(HttpStatusCode.NotModified);
155+
}
156+
157+
// Build bundle response (mimics LRM Cloud OTA API format)
158+
var bundle = new
159+
{
160+
version = _versionTimestamp.ToString("O"),
161+
project = "@demo/sample-app",
162+
defaultLanguage = "en",
163+
languages = _translations.Keys.ToList(),
164+
deleted = new List<string>(),
165+
translations = _translations
166+
};
167+
168+
var json = JsonSerializer.Serialize(bundle, new JsonSerializerOptions
169+
{
170+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
171+
WriteIndented = false
172+
});
173+
174+
var response = new HttpResponseMessage(HttpStatusCode.OK)
175+
{
176+
Content = new StringContent(json, Encoding.UTF8, "application/json")
177+
};
178+
179+
// Set ETag header for caching
180+
response.Headers.ETag = new EntityTagHeaderValue($"\"{etag}\"");
181+
182+
return response;
183+
}
184+
185+
private HttpResponseMessage HandleVersionRequest()
186+
{
187+
var version = new { version = _versionTimestamp.ToString("O") };
188+
var json = JsonSerializer.Serialize(version);
189+
190+
return new HttpResponseMessage(HttpStatusCode.OK)
191+
{
192+
Content = new StringContent(json, Encoding.UTF8, "application/json")
193+
};
194+
}
195+
196+
private static string ComputeETag(string version)
197+
{
198+
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(version));
199+
return Convert.ToHexString(bytes)[..16].ToLowerInvariant();
200+
}
201+
}

0 commit comments

Comments
 (0)