JD2022-TU1/main/tools/services/JD.LocalizationExport.Service/Implementation/LocalizationExportService.cs

370 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using JD.LocalizationExport.Service.API;
using JD.LocalizationExport.Service.Implementation.Exporters;
using JD.LocalizationExport.Service.Implementation.IntermediaryFile;
using JD.LocalizationExport.Service.Implementation.Secrets;
using JD.LocalizationExport.Service.Implementation.Serialization;
using JD.LocalizationExport.Service.Implementation.Server;
using JD.LocalizationExport.Service.Model;
using JD.LocalizationExport.Service.Model.Options;
using Newtonsoft.Json;
using Plume.Core.Logging;
using Plume.Interfaces.Services;
namespace JD.LocalizationExport.Service.Implementation
{
public class LocalizationExportService : ILocalizationExportService
{
private const string URLToCallOnOnlineLocalizationUploadFormat = @"http://backoffice-prod.tools.just-dance.com/Tools/loc/{0}";
private static readonly Dictionary<ServiceContentEnvironment, string> EnvironmentServerNames = new Dictionary<ServiceContentEnvironment, string>()
{
{ ServiceContentEnvironment.Dev, "dev" },
{ ServiceContentEnvironment.Prod, "prod" },
};
private ILogger _logger;
// The private part can seem useless, but only way to make the property readonly while initializing it outside of the constructor
private Dictionary<ExportType, ExportConfig> _exportConfigs;
public Dictionary<ExportType, ExportConfig> ExportConfigs => _exportConfigs;
public ExportEventsWrapper ExportEvents { get; } = new ExportEventsWrapper();
public string ConfigPath { get; } = @"Resources/ExportConfig.json";
public void Dispose()
{
// Nothing to dispose
}
public void Initialize(IServiceContext serviceContext)
{
_logger = serviceContext.Logger ?? throw new ArgumentNullException(nameof(serviceContext.Logger));
_logger.Info("Initializing export service...");
string fileConfig = File.ReadAllText(ConfigPath);
FullExportConfiguration config = JsonConvert.DeserializeObject<FullExportConfiguration>(fileConfig);
_exportConfigs = config.ExportConfigs;
_logger.Info("Export service initialized");
}
#region ILocalizationExportService implementation
public async Task<bool> ExportConsoleLocalizationAsync(DataToExport dataToExport, ConsoleExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
{
bool hasExportSucceeded = true;
try
{
string fullExportFolderPath = Path.GetFullPath(Path.Combine(repositoryRoot, ExportConfigs[ExportType.Console].ExportFolderPath));
LocalizationExporter exporter = new ConsoleLocalizationExporter(fullExportFolderPath, repositoryRoot, _logger, ExportEvents);
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, null).ConfigureAwait(false);
}
catch (Exception e)
{
_logger.Error(e.Message);
await Task.FromException(e);
}
return hasExportSucceeded;
}
public async Task<bool> ExportOnlineLocalizationAsync(DataToExport dataToExport, OnlineExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
{
bool hasExportSucceeded = true;
bool isTestExport = false;
string intermediaryFileAddressFormat = ExportConfigs[ExportType.Online].SaveFilePath;
var outputPath = Path.Combine(Path.GetTempPath(), "localisation.js"); // TODO: return exported files from ExportAsync to get them in the onLocalisationExportSuccess callback
try
{
LocalizationExporter exporter = new OnlineLocalizationExporter(Path.GetTempPath(), _logger, ExportEvents);
bool uploadToDevelopmentServers = exportOptions.UploadToDevServers;
bool uploadToProductionServers = exportOptions.UploadToProdServers;
bool forceUpload = exportOptions.ForceUpload;
List<ServiceContentEnvironment> serversToExportTo = new List<ServiceContentEnvironment>(Enum.GetNames(typeof(ServiceContentEnvironment)).Length);
if (uploadToDevelopmentServers)
{
serversToExportTo.Add(ServiceContentEnvironment.Dev);
}
if (uploadToProductionServers)
{
// Ask if the user is certain to export to avoid accidental exports on prod
System.Windows.MessageBoxResult answer = System.Windows.MessageBox.Show($"Are you sure you want to export on prod? {Environment.NewLine}" +
$"{Environment.NewLine}" +
$"This will change the texts seen by current players.",
"Export on production server",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Warning);
if (answer != System.Windows.MessageBoxResult.OK)
{
string exportCanceledMessage = "Online localisation export canceled";
System.Windows.MessageBox.Show(exportCanceledMessage,
"Export canceled",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Error);
throw new Exception(exportCanceledMessage);
}
serversToExportTo.Add(ServiceContentEnvironment.Prod);
}
if (serversToExportTo.Count == 0)
{
_logger.Warn("No upload type selected: making a test export without uploading it");
isTestExport = true;
serversToExportTo.Add(ServiceContentEnvironment.Dev);
}
foreach (ServiceContentEnvironment serverType in serversToExportTo)
{
// Updating intermediary file address
ExportConfigs[ExportType.Online].SaveFilePath = string.Format(intermediaryFileAddressFormat, EnvironmentServerNames[serverType]);
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, async () =>
{
bool hasUploadSucceeded = true;
if (!isTestExport && (uploadToDevelopmentServers || uploadToProductionServers)) // Double-check here so that we can never export by mistake
{
void LogInfo(string message)
{
_logger.Info(message);
}
_logger.Info("Getting credentials to connect to online server");
var secretsManager = await SecretsManagement.GetSecretsManager(_logger);
var configuration = await secretsManager.GetOnlineConfiguration();
_logger.Info("Successfully retrieved credentials to connect to online server");
var client = new GameServerClient();
ServerName serverName = serverType == ServiceContentEnvironment.Prod ? ServerName.Prod : ServerName.Uat;
await client.InitializeAsync(serverName, LogInfo, configuration);
async Task OnLocalizationUploadAsync(ServiceContentEnvironment s)
{
string server = EnvironmentServerNames[s];
string urlToCallOnLocalizationUpload = string.Format(URLToCallOnOnlineLocalizationUploadFormat, server);
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlToCallOnLocalizationUpload);
HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
_logger.Info("Successfully updated localization history on GitLab");
}
else
{
_logger.Error($"Attempt to update localization history failed with status code {response.StatusCode}");
hasUploadSucceeded = false;
}
}
catch (Exception e)
{
_logger.Error("Exception during request on localization upload, please contact a backoffice or tools developer");
_logger.Error(e.Message);
hasUploadSucceeded = false;
}
}
IProgress<double> onlineUploadProgress = new Progress<double>(d =>
{
int progressValue = (int)(100d * d);
progress.Report(Tuple.Create($"Online upload: {progressValue}%", progressValue));
});
// Upload
_logger.Info($"Uploading localization on UAT-{EnvironmentServerNames[serverType]}");
await client.UploadLocalizationAsync(outputPath,
serverType,
forceUpload,
LogInfo,
onlineUploadProgress,
async () => await OnLocalizationUploadAsync(serverType).ConfigureAwait(false))
.ConfigureAwait(false);
}
return hasUploadSucceeded;
}, isTestExport).ConfigureAwait(false);
}
}
catch (Exception e)
{
_logger.Error(e.Message);
await Task.FromException(e);
}
return hasExportSucceeded;
}
public async Task<bool> ExportMCALocalizationAsync(DataToExport dataToExport, MCAExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
{
bool hasExportSucceeded = true;
try
{
string fullExportFolderPath = Path.GetFullPath(Path.Combine(repositoryRoot, ExportConfigs[ExportType.MCA].ExportFolderPath));
LocalizationExporter exporter = new MCALocalizationExporter(fullExportFolderPath, _logger, ExportEvents);
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, null).ConfigureAwait(false);
}
catch (Exception e)
{
_logger.Error(e.Message);
await Task.FromException(e);
}
return hasExportSucceeded;
}
#endregion
private async Task<bool> ExportLocalizationAsync(LocalizationExporter exporter,
DataToExport dataToExport,
string repositoryRoot,
IProgress<Tuple<string, int>> progress,
Func<Task<bool>> onLocalisationExportSuccess,
bool isTestExport = false) // TODO: Would be better with a debug option for the export
{
bool hasExportSucceeded = true;
if (dataToExport == null)
throw new ArgumentNullException(nameof(dataToExport));
if (progress == null)
throw new ArgumentNullException(nameof(progress));
try
{
Line[] fullLinesToExport = { };
Language[] fullLanguagesToExport = { };
Language[] minLanguagesToExport = dataToExport.LanguagesToExport.ToArray();
ExportType currentExportType = exporter.ExportType;
ExportConfig currentConfig = ExportConfigs[currentExportType];
await exporter.ExportEvents.InvokeExportStartedAsync(currentExportType);
string locIntermediaryFile;
if (Uri.TryCreate(currentConfig.SaveFilePath, UriKind.Absolute, out Uri locIntermediaryFileUri))
{
if (locIntermediaryFileUri.Scheme == Uri.UriSchemeHttp || locIntermediaryFileUri.Scheme == Uri.UriSchemeHttps)
{
locIntermediaryFile = Path.Combine(Path.GetTempPath(), $"{currentExportType}Save.json");
await exporter.ExportEvents.InvokeExportFileStartedAsync(currentExportType, locIntermediaryFile, locIntermediaryFileUri);
}
else
{
throw new UriFormatException($"The path '{currentConfig.SaveFilePath}' is not a relative file path or an URL. Fix it in the configuration {ConfigPath}.");
}
}
else
{
locIntermediaryFile = Path.Combine(repositoryRoot, currentConfig.ExportFolderPath, currentConfig.SaveFilePath);
await exporter.ExportEvents.InvokeExportFileStartedAsync(currentExportType, locIntermediaryFile);
}
IIntermediaryFileManager intermediaryFileManager = new IntermediaryFileManager(locIntermediaryFile);
if (File.Exists(locIntermediaryFile))
{
fullLinesToExport = dataToExport.LinesToExport.ToArray();
fullLanguagesToExport = minLanguagesToExport;
_logger.Info("Loading previous export info...");
Tuple<Line[], Language[]> updatedExportData = await intermediaryFileManager.LoadFileAndMergeAsync(fullLinesToExport, fullLanguagesToExport, dataToExport.AllLanguages)
.ConfigureAwait(false);
fullLinesToExport = updatedExportData.Item1;
fullLanguagesToExport = updatedExportData.Item2;
}
else
{
// Need to export all lines in all languages to get a starting point for the intermediary file
System.Windows.MessageBoxResult answer = System.Windows.MessageBox.Show($"The {currentExportType.ToString()} localisation save file is missing. {Environment.NewLine}" +
$"Expected file: {Path.GetFullPath(locIntermediaryFile)} {Environment.NewLine}" +
$"{Environment.NewLine}" +
$"Without it, impossible to export only some lines or some languages. {Environment.NewLine}" +
$"{Environment.NewLine}" +
$"Export all lines in all languages?",
"Export all lines",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Question);
// TODO: Fix freeze just after this popup
if (answer == System.Windows.MessageBoxResult.OK)
{
Directory.CreateDirectory(Path.GetDirectoryName(locIntermediaryFile)); // Creating folder hierarchy if it doesn't exist
fullLinesToExport = dataToExport.AllLines.ToArray();
fullLanguagesToExport = dataToExport.AllLanguages.ToArray();
}
else
{
_logger.Error("No save file available, export aborted.");
hasExportSucceeded = false;
}
}
if (hasExportSucceeded)
{
_logger.Info("Beginning export...");
await exporter.ExportAsync(fullLinesToExport, fullLanguagesToExport, minLanguagesToExport).ConfigureAwait(false);
if (onLocalisationExportSuccess != null)
{
hasExportSucceeded &= await onLocalisationExportSuccess().ConfigureAwait(false);
}
if (hasExportSucceeded)
{
_logger.Info("Saving export info...");
await intermediaryFileManager.SaveFileAsync(fullLinesToExport, fullLanguagesToExport).ConfigureAwait(false);
_logger.Info("Saved");
if (locIntermediaryFileUri != null)
{
// Avoid permanent modification of remote data just for testing purposes
if (!isTestExport)
{
await exporter.ExportEvents.InvokeExportFileEndedAsync(currentExportType, locIntermediaryFile, locIntermediaryFileUri);
}
}
else
{
await exporter.ExportEvents.InvokeExportFileEndedAsync(currentExportType, locIntermediaryFile);
}
}
}
await exporter.ExportEvents.InvokeExportEndedAsync(currentExportType);
}
catch (Exception e)
{
_logger.Error(e.Message);
await Task.FromException(e);
}
return hasExportSucceeded;
}
}
}