Skip to content

Commit

Permalink
allow editing fwdata files directly without importing them
Browse files Browse the repository at this point in the history
  • Loading branch information
hahn-kev committed Jun 6, 2024
1 parent 681569c commit 8704561
Show file tree
Hide file tree
Showing 19 changed files with 288 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FwDataMiniLcmBridge.Api.UpdateProxy;
using Microsoft.Extensions.Logging;
using MiniLcm;
using SIL.LCModel;
using SIL.LCModel.Core.KernelInterfaces;
Expand All @@ -8,7 +9,7 @@

namespace FwDataMiniLcmBridge.Api;

public class LexboxLcmApi(LcmCache cache, bool onCloseSave) : ILexboxApi, IDisposable
public class FwDataMiniLcmApi(LcmCache cache, bool onCloseSave, ILogger<FwDataMiniLcmApi> logger) : ILexboxApi, IDisposable
{
private readonly IRepository<ILexEntry> _entriesRepository =
cache.ServiceLocator.GetInstance<IRepository<ILexEntry>>();
Expand All @@ -31,7 +32,11 @@ public class LexboxLcmApi(LcmCache cache, bool onCloseSave) : ILexboxApi, IDispo
public void Dispose()
{
if (onCloseSave)
{
logger.LogInformation("Saving FW data file {Name}", cache.ProjectId.Name);
cache.ActionHandlerAccessor.Commit();
}

cache.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateDictionaryProxy(ITsMultiString multiString, LexboxLcmApi lexboxLcmApi)
public class UpdateDictionaryProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi)
: IDictionary<WritingSystemId, string>, IDictionary
{
public void Add(KeyValuePair<WritingSystemId, string> item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateEntryProxy(ILexEntry lcmEntry, LexboxLcmApi lexboxLcmApi) : Entry
public class UpdateEntryProxy(ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) : Entry
{
public override Guid Id
{
Expand Down Expand Up @@ -48,12 +48,12 @@ public override MultiString Note
}
}

public class UpdateMultiStringProxy(ITsMultiString multiString, LexboxLcmApi lexboxLcmApi) : MultiString
public class UpdateMultiStringProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) : MultiString
{
public override IDictionary<WritingSystemId, string> Values { get; } = new UpdateDictionaryProxy(multiString, lexboxLcmApi);

public override MultiString Copy()
{
return new UpdateMultiStringProxy(multiString, lexboxLcmApi);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateExampleSentenceProxy(ILexExampleSentence sentence, LexboxLcmApi lexboxLcmApi): ExampleSentence
public class UpdateExampleSentenceProxy(ILexExampleSentence sentence, FwDataMiniLcmApi lexboxLcmApi): ExampleSentence
{
public override Guid Id
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateSenseProxy(ILexSense sense, LexboxLcmApi lexboxLcmApi) : Sense
public class UpdateSenseProxy(ILexSense sense, FwDataMiniLcmApi lexboxLcmApi) : Sense
{
public override Guid Id
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using MiniLcm;
using Mono.Unix.Native;
using FwDataMiniLcmBridge.LcmUtils;
using MiniLcm;

namespace FwDataMiniLcmBridge.LcmUtils;
namespace FwDataMiniLcmBridge;

public class FieldWorksProjectList
{
Expand All @@ -12,13 +12,12 @@ public static IEnumerable<IProjectIdentifier> EnumerateProjects()
var projectName = Path.GetFileName(directory);
if (string.IsNullOrEmpty(projectName)) continue;
if (!File.Exists(Path.Combine(directory, projectName + ".fwdata"))) continue;
yield return new FwDataProject(projectName);
yield return new FwDataProject(projectName, projectName + ".fwdata");
}
}
}

public class FwDataProject(string name) : IProjectIdentifier
{
public string Name { get; } = name;
public string Origin { get; } = "FieldWorks";
public static FwDataProject? GetProject(string name)
{
return EnumerateProjects().OfType<FwDataProject>().FirstOrDefault(p => p.Name == name);
}
}
19 changes: 19 additions & 0 deletions backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FwDataMiniLcmBridge.LcmUtils;
using Microsoft.Extensions.DependencyInjection;
using MiniLcm;

namespace FwDataMiniLcmBridge;

public static class FwDataBridgeKernel
{
public const string FwDataApiKey = "FwDataApiKey";
public static IServiceCollection AddFwDataBridge(this IServiceCollection services)
{
services.AddSingleton<FwDataFactory>();
//todo since this is scoped it gets created on each request (or hub method call), which opens the project file on each request
//this is not ideal since opening the project file can be slow. It should be done once per hub connection.
services.AddKeyedScoped<ILexboxApi>(FwDataApiKey, (provider, o) => provider.GetRequiredService<FwDataFactory>().GetCurrentFwDataMiniLcmApi());
services.AddSingleton<FwDataProjectContext>();
return services;
}
}
30 changes: 30 additions & 0 deletions backend/FwDataMiniLcmBridge/FwDataFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.LcmUtils;
using Microsoft.Extensions.Logging;
using MiniLcm;

namespace FwDataMiniLcmBridge;

public class FwDataFactory(FwDataProjectContext context, ILogger<FwDataMiniLcmApi> logger)
{
public FwDataMiniLcmApi GetFwDataMiniLcmApi(string projectName, bool saveOnDispose)
{
var project = FieldWorksProjectList.GetProject(projectName) ?? throw new InvalidOperationException($"Project {projectName} not found.");
return GetFwDataMiniLcmApi(project, saveOnDispose);
}
public FwDataMiniLcmApi GetFwDataMiniLcmApi(FwDataProject project, bool saveOnDispose)
{
var lcmCache = ProjectLoader.LoadCache(project.FileName);
return new FwDataMiniLcmApi(lcmCache, saveOnDispose, logger);
}

public FwDataMiniLcmApi GetCurrentFwDataMiniLcmApi()
{
var fwDataProject = context.Project;
if (fwDataProject is null)
{
throw new InvalidOperationException("No project is set in the context.");
}
return GetFwDataMiniLcmApi(fwDataProject, true);
}
}
2 changes: 2 additions & 0 deletions backend/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="SIL.LCModel" Version="11.0.0-beta0083"/>
<PackageReference Include="SIL.LCModel.Core" Version="11.0.0-beta0083"/>
<PackageReference Include="SIL.LCModel.Utils" Version="11.0.0-beta0083"/>
Expand Down
10 changes: 10 additions & 0 deletions backend/FwDataMiniLcmBridge/FwDataProject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using MiniLcm;

namespace FwDataMiniLcmBridge;

public class FwDataProject(string name, string fileName) : IProjectIdentifier
{
public string Name { get; } = name;
public string FileName { get; } = fileName;
public string Origin { get; } = "FieldWorks";
}
32 changes: 32 additions & 0 deletions backend/FwDataMiniLcmBridge/FwDataProjectContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace FwDataMiniLcmBridge;

public class FwDataProjectContext
{
private sealed class ProjectHolder
{
public FwDataProject? Project;
}

private static readonly AsyncLocal<ProjectHolder> _projectHolder = new();

public virtual FwDataProject? Project
{
get => _projectHolder.Value?.Project;
set
{
var holder = _projectHolder.Value;
if (holder != null)
{
// Clear current Project trapped in the AsyncLocals, as its done.
holder.Project = null;
}

if (value is not null)
{
// Use an object indirection to hold the Project in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared above.
_projectHolder.Value = new ProjectHolder { Project = value };
}
}
}
}
12 changes: 10 additions & 2 deletions backend/LocalWebApp/HttpHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
namespace LocalWebApp;
using LocalWebApp.Hubs;

namespace LocalWebApp;

public static class HttpHelpers
{
public static string? GetProjectName(this HttpContext? context)
{
var name = context?.Request.RouteValues.GetValueOrDefault(LexboxApiHub.ProjectRouteKey, null)?.ToString();
var name = context?.Request.RouteValues.GetValueOrDefault(CrdtMiniLcmApiHub.ProjectRouteKey, null)?.ToString();
return string.IsNullOrWhiteSpace(name) ? null : name;
}

public static string? GetFwDataName(this HttpContext? context)
{
var name = context?.Request.RouteValues.GetValueOrDefault(FwDataMiniLcmHub.ProjectRouteKey, null)?.ToString();
return string.IsNullOrWhiteSpace(name) ? null : name;
}
}
118 changes: 118 additions & 0 deletions backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using FwDataMiniLcmBridge;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using MiniLcm;
using SystemTextJsonPatch;

namespace LocalWebApp.Hubs;

public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi) : Hub<ILexboxClient>
{
public const string ProjectRouteKey = "fwdata";
public override async Task OnConnectedAsync()
{
}

public async Task<WritingSystems> GetWritingSystems()
{
return await lexboxApi.GetWritingSystems();
}

public async Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem)
{
var newWritingSystem = await lexboxApi.CreateWritingSystem(type, writingSystem);
return newWritingSystem;
}

public async Task<WritingSystem> UpdateWritingSystem(WritingSystemId id, WritingSystemType type, JsonPatchDocument<WritingSystem> update)
{
var writingSystem = await lexboxApi.UpdateWritingSystem(id, type, new JsonPatchUpdateInput<WritingSystem>(update));
return writingSystem;
}

public IAsyncEnumerable<Entry> GetEntriesForExemplar(string exemplar, QueryOptions? options = null)
{
throw new NotImplementedException();
}

public IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null)
{
return lexboxApi.GetEntries(options);
}

public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
{
return lexboxApi.SearchEntries(query, options);
}

public async Task<Entry?> GetEntry(Guid id)
{
return await lexboxApi.GetEntry(id);
}

public async Task<Entry> CreateEntry(Entry entry)
{
var newEntry = await lexboxApi.CreateEntry(entry);
await NotifyEntryUpdated(newEntry);
return newEntry;
}

public async Task<Entry> UpdateEntry(Guid id, JsonPatchDocument<Entry> update)
{
var entry = await lexboxApi.UpdateEntry(id, new JsonPatchUpdateInput<Entry>(update));
await NotifyEntryUpdated(entry);
return entry;
}

public async Task DeleteEntry(Guid id)
{
await lexboxApi.DeleteEntry(id);
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense)
{
var createdSense = await lexboxApi.CreateSense(entryId, sense);
return createdSense;
}

public async Task<Sense> UpdateSense(Guid entryId, Guid senseId, JsonPatchDocument<Sense> update)
{
var sense = await lexboxApi.UpdateSense(entryId, senseId, new JsonPatchUpdateInput<Sense>(update));
return sense;
}

public async Task DeleteSense(Guid entryId, Guid senseId)
{
await lexboxApi.DeleteSense(entryId, senseId);
}

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId,
Guid senseId,
ExampleSentence exampleSentence)
{
var createdSentence = await lexboxApi.CreateExampleSentence(entryId, senseId, exampleSentence);
return createdSentence;
}

public async Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
Guid senseId,
Guid exampleSentenceId,
JsonPatchDocument<ExampleSentence> update)
{
var sentence = await lexboxApi.UpdateExampleSentence(entryId,
senseId,
exampleSentenceId,
new JsonPatchUpdateInput<ExampleSentence>(update));
return sentence;
}

public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
{
await lexboxApi.DeleteExampleSentence(entryId, senseId, exampleSentenceId);
}

private async Task NotifyEntryUpdated(Entry entry)
{
await Clients.Others.OnEntryUpdated(entry);
}
}
6 changes: 5 additions & 1 deletion backend/LocalWebApp/LocalAppKernel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using Crdt;
using FwDataMiniLcmBridge;
using LcmCrdt;
using LocalWebApp.Services;
using Microsoft.AspNetCore.Http.Json;
Expand All @@ -11,14 +12,16 @@ namespace LocalWebApp;

public static class LocalAppKernel
{
public static void AddLocalAppServices(this IServiceCollection services)
public static IServiceCollection AddLocalAppServices(this IServiceCollection services)
{
services.AddSingleton<BackgroundSyncService>();
services.AddHttpContextAccessor();
services.AddScoped<SyncService>();
services.AddSingleton<ImportFwdataService>();
services.AddSingleton<IHostedService>(s => s.GetRequiredService<BackgroundSyncService>());
services.AddLcmCrdtClient();
services.AddFwDataBridge();

services.AddOptions<JsonOptions>().PostConfigure<IOptions<CrdtConfig>>((jsonOptions, crdtConfig) =>
{
jsonOptions.SerializerOptions.TypeInfoResolver = crdtConfig.Value.MakeJsonTypeResolver();
Expand All @@ -39,5 +42,6 @@ public static void AddLocalAppServices(this IServiceCollection services)
})
});
services.AddSingleton<CrdtHttpSyncService>();
return services;
}
}
Loading

0 comments on commit 8704561

Please sign in to comment.