< Summary

Information
Class: Chronicis.Api.Services.UserService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/UserService.cs
Line coverage
100%
Covered lines: 14
Uncovered lines: 0
Coverable lines: 14
Total lines: 606
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
RemapArticleIdsInText(...)100%66100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/UserService.cs

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Chronicis.Shared.Models;
 5using Chronicis.Shared.Utilities;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace Chronicis.Api.Services;
 9
 10/// <summary>
 11/// Implementation of user management service
 12/// </summary>
 13public sealed class UserService : IUserService
 14{
 115    private static readonly Guid TutorialTemplateWorldId = new("bbcee097-e733-4c55-a72b-91fa2cfa0391");
 16
 17    private readonly ChronicisDbContext _context;
 18    private readonly IWorldService _worldService;
 19    private readonly ILogger<UserService> _logger;
 20
 21    public UserService(
 22        ChronicisDbContext context,
 23        IWorldService worldService,
 24        ILogger<UserService> logger)
 25    {
 1626        _context = context;
 1627        _worldService = worldService;
 1628        _logger = logger;
 1629    }
 30
 31    public async Task<User> GetOrCreateUserAsync(
 32        string auth0UserId,
 33        string email,
 34        string displayName,
 35        string? avatarUrl)
 36    {
 37        // Try to find existing user
 38        var user = await _context.Users
 39            .FirstOrDefaultAsync(u => u.Auth0UserId == auth0UserId);
 40
 41        if (user == null)
 42        {
 43            // Create new user
 44            user = new User
 45            {
 46                Id = Guid.NewGuid(),
 47                Auth0UserId = auth0UserId,
 48                Email = email,
 49                DisplayName = displayName,
 50                AvatarUrl = avatarUrl,
 51                CreatedAt = DateTime.UtcNow,
 52                LastLoginAt = DateTime.UtcNow,
 53                HasCompletedOnboarding = false
 54            };
 55
 56            _context.Users.Add(user);
 57            await _context.SaveChangesAsync();
 58
 59            _logger.LogTraceSanitized("Created new user {UserId} for Auth0 ID {Auth0UserId}", user.Id, auth0UserId);
 60
 61            try
 62            {
 63                if (!await TryCloneTutorialWorldFromTemplateAsync(user.Id))
 64                {
 65                    _logger.LogWarningSanitized(
 66                        "Tutorial template world {TemplateWorldId} was not found. Falling back to generated default tuto
 67                        TutorialTemplateWorldId,
 68                        user.Id);
 69
 70                    await CreateDefaultWorldAsync(user.Id, isTutorial: true);
 71                }
 72            }
 73            catch (Exception ex)
 74            {
 75                // Do not block login if tutorial provisioning fails.
 76                _logger.LogErrorSanitized(ex, "Failed to provision tutorial world for new user {UserId}", user.Id);
 77            }
 78        }
 79        else
 80        {
 81            // Update user info in case it changed (e.g., user changed their name/avatar in Auth0)
 82            bool needsUpdate = false;
 83
 84            if (user.Email != email)
 85            {
 86                user.Email = email;
 87                needsUpdate = true;
 88            }
 89
 90            if (user.DisplayName != displayName)
 91            {
 92                user.DisplayName = displayName;
 93                needsUpdate = true;
 94            }
 95
 96            if (user.AvatarUrl != avatarUrl)
 97            {
 98                user.AvatarUrl = avatarUrl;
 99                needsUpdate = true;
 100            }
 101
 102            // Always update last login
 103            user.LastLoginAt = DateTime.UtcNow;
 104            needsUpdate = true;
 105
 106            if (needsUpdate)
 107            {
 108                await _context.SaveChangesAsync();
 109            }
 110        }
 111
 112        return user;
 113    }
 114
 115    public async Task<User?> GetUserByIdAsync(Guid userId)
 116    {
 117        return await _context.Users.FindAsync(userId);
 118    }
 119
 120    public async Task UpdateLastLoginAsync(Guid userId)
 121    {
 122        var user = await _context.Users.FindAsync(userId);
 123        if (user != null)
 124        {
 125            user.LastLoginAt = DateTime.UtcNow;
 126            await _context.SaveChangesAsync();
 127        }
 128    }
 129
 130    public async Task<UserProfileDto?> GetUserProfileAsync(Guid userId)
 131    {
 132        var user = await _context.Users.FindAsync(userId);
 133        if (user == null)
 134        {
 135            return null;
 136        }
 137
 138        return new UserProfileDto
 139        {
 140            Id = user.Id,
 141            Email = user.Email,
 142            DisplayName = user.DisplayName,
 143            AvatarUrl = user.AvatarUrl,
 144            CreatedAt = user.CreatedAt,
 145            LastLoginAt = user.LastLoginAt,
 146            HasCompletedOnboarding = user.HasCompletedOnboarding
 147        };
 148    }
 149
 150    public async Task<bool> CompleteOnboardingAsync(Guid userId)
 151    {
 152        var user = await _context.Users.FindAsync(userId);
 153        if (user == null)
 154        {
 155            _logger.LogWarningSanitized("Attempted to complete onboarding for non-existent user {UserId}", userId);
 156            return false;
 157        }
 158
 159        if (!user.HasCompletedOnboarding)
 160        {
 161            user.HasCompletedOnboarding = true;
 162            await _context.SaveChangesAsync();
 163            _logger.LogTraceSanitized("User {UserId} completed onboarding", userId);
 164        }
 165
 166        return true;
 167    }
 168
 169    /// <summary>
 170    /// Creates a default world with root structure for a new user.
 171    /// Used as a fallback if the tutorial template world is unavailable.
 172    /// </summary>
 173    private async Task CreateDefaultWorldAsync(Guid userId, bool isTutorial = false)
 174    {
 175        _logger.LogTraceSanitized("Creating default world for user {UserId}", userId);
 176
 177        var createDto = new WorldCreateDto
 178        {
 179            Name = "My World",
 180            Description = "Your personal world for campaigns and adventures"
 181        };
 182
 183        var createdWorld = await _worldService.CreateWorldAsync(createDto, userId);
 184
 185        if (!isTutorial)
 186        {
 187            return;
 188        }
 189
 190        var world = await _context.Worlds.FindAsync(createdWorld.Id);
 191        if (world != null && !world.IsTutorial)
 192        {
 193            world.IsTutorial = true;
 194            await _context.SaveChangesAsync();
 195        }
 196    }
 197
 198    private async Task<bool> TryCloneTutorialWorldFromTemplateAsync(Guid userId)
 199    {
 200        var templateWorld = await _context.Worlds
 201            .AsNoTracking()
 202            .FirstOrDefaultAsync(w => w.Id == TutorialTemplateWorldId);
 203
 204        if (templateWorld == null)
 205        {
 206            return false;
 207        }
 208
 209        var templateCampaigns = await _context.Campaigns
 210            .AsNoTracking()
 211            .Where(c => c.WorldId == templateWorld.Id)
 212            .ToListAsync();
 213        var templateCampaignIds = templateCampaigns.Select(c => c.Id).ToList();
 214
 215        var templateArcs = await _context.Arcs
 216            .AsNoTracking()
 217            .Where(a => templateCampaignIds.Contains(a.CampaignId))
 218            .ToListAsync();
 219        var templateArcIds = templateArcs.Select(a => a.Id).ToList();
 220
 221        var templateSessions = await _context.Sessions
 222            .AsNoTracking()
 223            .Where(s => templateArcIds.Contains(s.ArcId))
 224            .ToListAsync();
 225        var templateSessionIds = templateSessions.Select(s => s.Id).ToList();
 226
 227        var templateQuests = await _context.Quests
 228            .AsNoTracking()
 229            .Where(q => templateArcIds.Contains(q.ArcId))
 230            .ToListAsync();
 231        var templateQuestIds = templateQuests.Select(q => q.Id).ToList();
 232
 233        var templateQuestUpdates = await _context.QuestUpdates
 234            .AsNoTracking()
 235            .Where(qu => templateQuestIds.Contains(qu.QuestId))
 236            .ToListAsync();
 237
 238        var templateSummaryTemplates = await _context.SummaryTemplates
 239            .AsNoTracking()
 240            .Where(st => st.WorldId == templateWorld.Id)
 241            .ToListAsync();
 242
 243        var templateArticles = await _context.Articles
 244            .AsNoTracking()
 245            .Where(a =>
 246                a.WorldId == templateWorld.Id ||
 247                (a.CampaignId.HasValue && templateCampaignIds.Contains(a.CampaignId.Value)) ||
 248                (a.ArcId.HasValue && templateArcIds.Contains(a.ArcId.Value)) ||
 249                (a.SessionId.HasValue && templateSessionIds.Contains(a.SessionId.Value)))
 250            .ToListAsync();
 251        var templateArticleIds = templateArticles.Select(a => a.Id).ToList();
 252
 253        var templateArticleAliases = await _context.ArticleAliases
 254            .AsNoTracking()
 255            .Where(aa => templateArticleIds.Contains(aa.ArticleId))
 256            .ToListAsync();
 257
 258        var templateArticleExternalLinks = await _context.ArticleExternalLinks
 259            .AsNoTracking()
 260            .Where(ael => templateArticleIds.Contains(ael.ArticleId))
 261            .ToListAsync();
 262
 263        var templateArticleLinks = await _context.ArticleLinks
 264            .AsNoTracking()
 265            .Where(al => templateArticleIds.Contains(al.SourceArticleId) && templateArticleIds.Contains(al.TargetArticle
 266            .ToListAsync();
 267
 268        var templateWorldLinks = await _context.WorldLinks
 269            .AsNoTracking()
 270            .Where(wl => wl.WorldId == templateWorld.Id)
 271            .ToListAsync();
 272
 273        var templateWorldResourceProviders = await _context.WorldResourceProviders
 274            .AsNoTracking()
 275            .Where(wrp => wrp.WorldId == templateWorld.Id)
 276            .ToListAsync();
 277
 278        var skippedDocumentCount = await _context.WorldDocuments
 279            .AsNoTracking()
 280            .CountAsync(d => d.WorldId == templateWorld.Id);
 281        if (skippedDocumentCount > 0)
 282        {
 283            _logger.LogWarningSanitized(
 284                "Tutorial template world {TemplateWorldId} contains {DocumentCount} world documents. Document rows are n
 285                TutorialTemplateWorldId,
 286                skippedDocumentCount);
 287        }
 288
 289        var now = DateTime.UtcNow;
 290        var newWorldId = Guid.NewGuid();
 291        var newWorldSlug = await GenerateUniqueWorldSlugAsync(templateWorld.Name, userId);
 292
 293        var summaryTemplateIdMap = templateSummaryTemplates.ToDictionary(st => st.Id, _ => Guid.NewGuid());
 294        var campaignIdMap = templateCampaigns.ToDictionary(c => c.Id, _ => Guid.NewGuid());
 295        var arcIdMap = templateArcs.ToDictionary(a => a.Id, _ => Guid.NewGuid());
 296        var sessionIdMap = templateSessions.ToDictionary(s => s.Id, _ => Guid.NewGuid());
 297        var questIdMap = templateQuests.ToDictionary(q => q.Id, _ => Guid.NewGuid());
 298        var articleIdMap = templateArticles.ToDictionary(a => a.Id, _ => Guid.NewGuid());
 299
 300        Guid? MapSummaryTemplate(Guid? templateSummaryTemplateId)
 301        {
 302            if (!templateSummaryTemplateId.HasValue)
 303            {
 304                return null;
 305            }
 306
 307            return summaryTemplateIdMap.TryGetValue(templateSummaryTemplateId.Value, out var mappedId)
 308                ? mappedId
 309                : templateSummaryTemplateId;
 310        }
 311
 312        Guid? MapOptionalGuid(Guid? templateId, IReadOnlyDictionary<Guid, Guid> idMap)
 313        {
 314            if (!templateId.HasValue)
 315            {
 316                return null;
 317            }
 318
 319            return idMap.TryGetValue(templateId.Value, out var mappedId)
 320                ? mappedId
 321                : null;
 322        }
 323
 324        var clonedWorld = new World
 325        {
 326            Id = newWorldId,
 327            Name = templateWorld.Name,
 328            Slug = newWorldSlug,
 329            Description = templateWorld.Description,
 330            OwnerId = userId,
 331            CreatedAt = now,
 332            IsTutorial = true,
 333            IsPublic = false
 334        };
 335
 336        var clonedSummaryTemplates = templateSummaryTemplates.Select(st => new SummaryTemplate
 337        {
 338            Id = summaryTemplateIdMap[st.Id],
 339            WorldId = newWorldId,
 340            Name = st.Name,
 341            Description = st.Description,
 342            PromptTemplate = st.PromptTemplate,
 343            IsSystem = st.IsSystem,
 344            CreatedBy = st.CreatedBy.HasValue ? userId : null,
 345            CreatedAt = st.CreatedAt
 346        }).ToList();
 347
 348        var clonedCampaigns = templateCampaigns.Select(c => new Campaign
 349        {
 350            Id = campaignIdMap[c.Id],
 351            WorldId = newWorldId,
 352            Name = c.Name,
 353            Description = c.Description,
 354            OwnerId = userId,
 355            CreatedAt = c.CreatedAt,
 356            StartedAt = c.StartedAt,
 357            EndedAt = c.EndedAt,
 358            IsActive = c.IsActive,
 359            SummaryTemplateId = MapSummaryTemplate(c.SummaryTemplateId),
 360            SummaryCustomPrompt = c.SummaryCustomPrompt,
 361            SummaryIncludeWebSources = c.SummaryIncludeWebSources,
 362            AISummary = c.AISummary,
 363            AISummaryGeneratedAt = c.AISummaryGeneratedAt
 364        }).ToList();
 365
 366        var clonedArcs = templateArcs.Select(a => new Arc
 367        {
 368            Id = arcIdMap[a.Id],
 369            CampaignId = campaignIdMap[a.CampaignId],
 370            Name = a.Name,
 371            Description = a.Description,
 372            SortOrder = a.SortOrder,
 373            CreatedAt = a.CreatedAt,
 374            CreatedBy = userId,
 375            IsActive = a.IsActive,
 376            SummaryTemplateId = MapSummaryTemplate(a.SummaryTemplateId),
 377            SummaryCustomPrompt = a.SummaryCustomPrompt,
 378            SummaryIncludeWebSources = a.SummaryIncludeWebSources,
 379            AISummary = a.AISummary,
 380            AISummaryGeneratedAt = a.AISummaryGeneratedAt
 381        }).ToList();
 382
 383        var clonedSessions = templateSessions.Select(s => new Session
 384        {
 385            Id = sessionIdMap[s.Id],
 386            ArcId = arcIdMap[s.ArcId],
 387            Name = s.Name,
 388            SessionDate = s.SessionDate,
 389            PublicNotes = s.PublicNotes,
 390            PrivateNotes = s.PrivateNotes,
 391            AiSummary = s.AiSummary,
 392            AiSummaryGeneratedAt = s.AiSummaryGeneratedAt,
 393            AiSummaryGeneratedByUserId = s.AiSummaryGeneratedByUserId.HasValue ? userId : null,
 394            CreatedAt = s.CreatedAt,
 395            ModifiedAt = s.ModifiedAt,
 396            CreatedBy = userId
 397        }).ToList();
 398
 399        var clonedQuests = templateQuests.Select(q => new Quest
 400        {
 401            Id = questIdMap[q.Id],
 402            ArcId = arcIdMap[q.ArcId],
 403            Title = q.Title,
 404            Description = q.Description,
 405            Status = q.Status,
 406            IsGmOnly = q.IsGmOnly,
 407            SortOrder = q.SortOrder,
 408            CreatedBy = userId,
 409            CreatedAt = q.CreatedAt,
 410            UpdatedAt = q.UpdatedAt,
 411            RowVersion = q.RowVersion?.ToArray() ?? Array.Empty<byte>()
 412        }).ToList();
 413
 414        var clonedQuestUpdates = templateQuestUpdates.Select(qu => new QuestUpdate
 415        {
 416            Id = Guid.NewGuid(),
 417            QuestId = questIdMap[qu.QuestId],
 418            SessionId = MapOptionalGuid(qu.SessionId, sessionIdMap),
 419            Body = qu.Body,
 420            CreatedBy = userId,
 421            CreatedAt = qu.CreatedAt
 422        }).ToList();
 423
 424        var clonedArticles = templateArticles.Select(a => new Article
 425        {
 426            Id = articleIdMap[a.Id],
 427            ParentId = MapOptionalGuid(a.ParentId, articleIdMap),
 428            WorldId = a.WorldId.HasValue ? newWorldId : null,
 429            CampaignId = MapOptionalGuid(a.CampaignId, campaignIdMap),
 430            ArcId = MapOptionalGuid(a.ArcId, arcIdMap),
 431            Title = a.Title,
 432            Slug = a.Slug,
 433            Body = a.Body,
 434            IconEmoji = a.IconEmoji,
 435            Type = a.Type,
 436            Visibility = a.Visibility,
 437            CreatedBy = userId,
 438            LastModifiedBy = a.LastModifiedBy.HasValue ? userId : null,
 439            CreatedAt = a.CreatedAt,
 440            ModifiedAt = a.ModifiedAt,
 441            SessionDate = a.SessionDate,
 442            InGameDate = a.InGameDate,
 443            SessionId = MapOptionalGuid(a.SessionId, sessionIdMap),
 444            PlayerId = a.PlayerId.HasValue ? userId : null,
 445            SummaryTemplateId = MapSummaryTemplate(a.SummaryTemplateId),
 446            SummaryCustomPrompt = a.SummaryCustomPrompt,
 447            SummaryIncludeWebSources = a.SummaryIncludeWebSources,
 448            AISummary = a.AISummary,
 449            AISummaryGeneratedAt = a.AISummaryGeneratedAt,
 450            EffectiveDate = a.EffectiveDate
 451        }).ToList();
 452
 453        // Remap embedded wiki-link target GUIDs (and any other copied article GUID references) in rich text.
 454        foreach (var article in clonedArticles)
 455        {
 456            article.Body = RemapArticleIdsInText(article.Body, articleIdMap);
 457        }
 458
 459        foreach (var session in clonedSessions)
 460        {
 461            session.PublicNotes = RemapArticleIdsInText(session.PublicNotes, articleIdMap);
 462            session.PrivateNotes = RemapArticleIdsInText(session.PrivateNotes, articleIdMap);
 463        }
 464
 465        foreach (var quest in clonedQuests)
 466        {
 467            quest.Description = RemapArticleIdsInText(quest.Description, articleIdMap);
 468        }
 469
 470        foreach (var questUpdate in clonedQuestUpdates)
 471        {
 472            questUpdate.Body = RemapArticleIdsInText(questUpdate.Body, articleIdMap) ?? questUpdate.Body;
 473        }
 474
 475        var clonedArticleAliases = templateArticleAliases.Select(aa => new ArticleAlias
 476        {
 477            Id = Guid.NewGuid(),
 478            ArticleId = articleIdMap[aa.ArticleId],
 479            AliasText = aa.AliasText,
 480            AliasType = aa.AliasType,
 481            EffectiveDate = aa.EffectiveDate,
 482            CreatedAt = aa.CreatedAt
 483        }).ToList();
 484
 485        var clonedArticleExternalLinks = templateArticleExternalLinks.Select(ael => new ArticleExternalLink
 486        {
 487            Id = Guid.NewGuid(),
 488            ArticleId = articleIdMap[ael.ArticleId],
 489            Source = ael.Source,
 490            ExternalId = ael.ExternalId,
 491            DisplayTitle = ael.DisplayTitle
 492        }).ToList();
 493
 494        var clonedArticleLinks = templateArticleLinks.Select(al => new ArticleLink
 495        {
 496            Id = Guid.NewGuid(),
 497            SourceArticleId = articleIdMap[al.SourceArticleId],
 498            TargetArticleId = articleIdMap[al.TargetArticleId],
 499            DisplayText = al.DisplayText,
 500            Position = al.Position,
 501            CreatedAt = al.CreatedAt
 502        }).ToList();
 503
 504        var clonedWorldLinks = templateWorldLinks.Select(wl => new WorldLink
 505        {
 506            Id = Guid.NewGuid(),
 507            WorldId = newWorldId,
 508            Url = wl.Url,
 509            Title = wl.Title,
 510            Description = wl.Description,
 511            CreatedAt = wl.CreatedAt
 512        }).ToList();
 513
 514        var clonedWorldResourceProviders = templateWorldResourceProviders.Select(wrp => new WorldResourceProvider
 515        {
 516            WorldId = newWorldId,
 517            ResourceProviderCode = wrp.ResourceProviderCode,
 518            IsEnabled = wrp.IsEnabled,
 519            LookupKey = wrp.LookupKey,
 520            ModifiedAt = wrp.ModifiedAt,
 521            ModifiedByUserId = userId
 522        }).ToList();
 523
 524        var ownerMembership = new WorldMember
 525        {
 526            Id = Guid.NewGuid(),
 527            WorldId = newWorldId,
 528            UserId = userId,
 529            Role = WorldRole.GM,
 530            JoinedAt = now,
 531            InvitedBy = null
 532        };
 533
 534        _context.Worlds.Add(clonedWorld);
 535        if (clonedSummaryTemplates.Count > 0)
 536            _context.SummaryTemplates.AddRange(clonedSummaryTemplates);
 537        if (clonedCampaigns.Count > 0)
 538            _context.Campaigns.AddRange(clonedCampaigns);
 539        if (clonedArcs.Count > 0)
 540            _context.Arcs.AddRange(clonedArcs);
 541        if (clonedSessions.Count > 0)
 542            _context.Sessions.AddRange(clonedSessions);
 543        if (clonedQuests.Count > 0)
 544            _context.Quests.AddRange(clonedQuests);
 545        if (clonedQuestUpdates.Count > 0)
 546            _context.QuestUpdates.AddRange(clonedQuestUpdates);
 547        if (clonedArticles.Count > 0)
 548            _context.Articles.AddRange(clonedArticles);
 549        if (clonedArticleAliases.Count > 0)
 550            _context.ArticleAliases.AddRange(clonedArticleAliases);
 551        if (clonedArticleExternalLinks.Count > 0)
 552            _context.ArticleExternalLinks.AddRange(clonedArticleExternalLinks);
 553        if (clonedArticleLinks.Count > 0)
 554            _context.ArticleLinks.AddRange(clonedArticleLinks);
 555        if (clonedWorldLinks.Count > 0)
 556            _context.WorldLinks.AddRange(clonedWorldLinks);
 557        if (clonedWorldResourceProviders.Count > 0)
 558            _context.WorldResourceProviders.AddRange(clonedWorldResourceProviders);
 559        _context.WorldMembers.Add(ownerMembership);
 560
 561        await _context.SaveChangesAsync();
 562
 563        _logger.LogTraceSanitized(
 564            "Provisioned tutorial world {WorldId} from template {TemplateWorldId} for user {UserId} (Campaigns={Campaign
 565            newWorldId,
 566            TutorialTemplateWorldId,
 567            userId,
 568            clonedCampaigns.Count,
 569            clonedArcs.Count,
 570            clonedSessions.Count,
 571            clonedArticles.Count);
 572
 573        return true;
 574    }
 575
 576    private async Task<string> GenerateUniqueWorldSlugAsync(string worldName, Guid ownerId)
 577    {
 578        var baseSlug = SlugGenerator.GenerateSlug(worldName);
 579        var existingSlugs = await _context.Worlds
 580            .AsNoTracking()
 581            .Where(w => w.OwnerId == ownerId)
 582            .Select(w => w.Slug)
 583            .ToHashSetAsync();
 584
 585        return SlugGenerator.GenerateUniqueSlug(baseSlug, existingSlugs);
 586    }
 587
 588    private static string? RemapArticleIdsInText(string? text, IReadOnlyDictionary<Guid, Guid> articleIdMap)
 589    {
 9590        if (string.IsNullOrEmpty(text) || articleIdMap.Count == 0)
 591        {
 2592            return text;
 593        }
 594
 7595        var remapped = text;
 56596        foreach (var kvp in articleIdMap)
 597        {
 21598            remapped = remapped.Replace(
 21599                kvp.Key.ToString(),
 21600                kvp.Value.ToString(),
 21601                StringComparison.OrdinalIgnoreCase);
 602        }
 603
 7604        return remapped;
 605    }
 606}