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 EnvironmentServerNames = new Dictionary() { { 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 _exportConfigs; public Dictionary 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(fileConfig); _exportConfigs = config.ExportConfigs; _logger.Info("Export service initialized"); } #region ILocalizationExportService implementation public async Task ExportConsoleLocalizationAsync(DataToExport dataToExport, ConsoleExportOptions exportOptions, string repositoryRoot, IProgress> 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 ExportOnlineLocalizationAsync(DataToExport dataToExport, OnlineExportOptions exportOptions, string repositoryRoot, IProgress> 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 serversToExportTo = new List(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 onlineUploadProgress = new Progress(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 ExportMCALocalizationAsync(DataToExport dataToExport, MCAExportOptions exportOptions, string repositoryRoot, IProgress> 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 ExportLocalizationAsync(LocalizationExporter exporter, DataToExport dataToExport, string repositoryRoot, IProgress> progress, Func> 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 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; } } }