Skip to content

Commit 916657b

Browse files
authored
Convert resource links to svn links (#1609)
Closes SoftUni-Internal/exam-systems-issues#1701 **Summary of the changes made**: 1. {Describe your changes in the list} 2.
1 parent 9bc6dce commit 916657b

File tree

7 files changed

+107
-56
lines changed

7 files changed

+107
-56
lines changed

Servers/UI/OJS.Servers.Ui/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public static void ConfigureServices(
3939
.AddOptionsWithValidation<ApplicationUrlsConfig>()
4040
.AddOptionsWithValidation<EmailServiceConfig>()
4141
.AddOptionsWithValidation<MentorConfig>()
42+
.AddOptionsWithValidation<SvnConfig>()
4243
.AddControllers();
4344
}
4445
}

Services/Infrastructure/OJS.Services.Infrastructure/Configurations/SvnConfig.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace OJS.Services.Infrastructure.Configurations;
22

3+
using System.Collections.Generic;
34
using System.ComponentModel.DataAnnotations;
45

56
public class SvnConfig : BaseConfig
@@ -14,4 +15,6 @@ public class SvnConfig : BaseConfig
1415

1516
[Required]
1617
public string Password { get; set; } = default!;
18+
19+
public IEnumerable<string>? AlternativeBaseUrls { get; set; }
1720
}

Services/Infrastructure/OJS.Services.Infrastructure/Constants/LoggerMessageDefinitions.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,33 @@ public static partial class LoggerMessageDefinitions
8686
[LoggerMessage(302, LogLevel.Information, "Truncated {PercentageOfMessageContentTruncated:F2}% of a message's content to comply with token limits for problem #{ProblemId}.")]
8787
public static partial void LogPercentageOfMessageContentTruncated(this ILogger logger, double percentageOfMessageContentTruncated, int problemId);
8888

89-
[LoggerMessage(303, LogLevel.Error, "The downloaded file from {Link} for problem #{ProblemId} in contest #{ContestId} is not in the expected format.", SkipEnabledCheck = true)]
89+
[LoggerMessage(303, LogLevel.Information, "Link: {OriginalLink} successfully converted to SVN link: {newLink}")]
90+
public static partial void LogLinkSuccessfullyConvertedToSvnLink(this ILogger logger, string originalLink, string newLink);
91+
92+
[LoggerMessage(340, LogLevel.Error, "The downloaded file from {Link} for problem #{ProblemId} in contest #{ContestId} is not in the expected format.", SkipEnabledCheck = true)]
9093
public static partial void LogInvalidDocumentFormat(this ILogger logger, int problemId, int contestId, string link);
9194

92-
[LoggerMessage(304, LogLevel.Error, "The downloaded file from {Link} for problem #{ProblemId} in contest #{ContestId} is either empty or does not exist.", SkipEnabledCheck = true)]
95+
[LoggerMessage(341, LogLevel.Error, "The downloaded file from {Link} for problem #{ProblemId} in contest #{ContestId} is either empty or does not exist.", SkipEnabledCheck = true)]
9396
public static partial void LogFileNotFoundOrEmpty(this ILogger logger, int problemId, int contestId, string link);
9497

95-
[LoggerMessage(305, LogLevel.Error, "Failed to download the file from {Link} for problem #{ProblemId} in contest #{ContestId} with status code {StatusCode} and response message: {ResponseMessage}.", SkipEnabledCheck = true)]
98+
[LoggerMessage(342, LogLevel.Error, "Failed to download the file from {Link} for problem #{ProblemId} in contest #{ContestId} with status code {StatusCode} and response message: {ResponseMessage}.", SkipEnabledCheck = true)]
9699
public static partial void LogHttpRequestFailure(this ILogger logger, int problemId, int contestId, HttpStatusCode statusCode, string link, string? responseMessage);
97100

98-
[LoggerMessage(306, LogLevel.Error, "Failed to download the file from {Link} for problem #{ProblemId} in contest #{ContestId}.", SkipEnabledCheck = true)]
101+
[LoggerMessage(343, LogLevel.Error, "Failed to download the file from {Link} for problem #{ProblemId} in contest #{ContestId}.", SkipEnabledCheck = true)]
99102
public static partial void LogResourceDownloadFailure(this ILogger logger, int problemId, int contestId, string link, Exception ex);
100103

101-
[LoggerMessage(307, LogLevel.Error, "Failed to parse the content of the downloaded file for problem #{ProblemId} in contest #{ContestId}.", SkipEnabledCheck = true)]
104+
[LoggerMessage(344, LogLevel.Error, "Failed to parse the content of the downloaded file for problem #{ProblemId} in contest #{ContestId}.", SkipEnabledCheck = true)]
102105
public static partial void LogFileParsingFailure(this ILogger logger, int problemId, int contestId);
103106

104-
[LoggerMessage(308, LogLevel.Error, "Problem description resource not found for problem #{ProblemId} in contest #{ContestId}. Verify that the problem or the first problem in the contest has a valid description resource.", SkipEnabledCheck = true)]
107+
[LoggerMessage(345, LogLevel.Error, "Problem description resource not found for problem #{ProblemId} in contest #{ContestId}. Verify that the problem or the first problem in the contest has a valid description resource.", SkipEnabledCheck = true)]
105108
public static partial void LogProblemDescriptionResourceNotFound(this ILogger logger, int problemId, int contestId);
106109

110+
[LoggerMessage(356, LogLevel.Error, "The SVN BaseUrl provided in settings is not valid. Expected valid absolute URL, but got: {SvnBaseUrl}", SkipEnabledCheck = true)]
111+
public static partial void LogSvnBaseUrlNotValid(this ILogger logger, string svnBaseUrl);
112+
113+
[LoggerMessage(380, LogLevel.Warning, "Couldn't find a valid alternative SVN base url in settings for link: {Link}.")]
114+
public static partial void LogAlternativeBaseUrlNotFoundForLink(this ILogger logger, string link);
115+
107116
// Submissions
108117
[LoggerMessage(1010, LogLevel.Error, "Exception in publishing submission #{SubmissionId}", SkipEnabledCheck = true)]
109118
public static partial void LogExceptionPublishingSubmission(this ILogger logger, int submissionId, Exception ex);

Services/Mentor/OJS.Services.Mentor.Business/Implementations/MentorBusinessService.cs

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using OJS.Services.Infrastructure.Extensions;
2222
using OJS.Services.Mentor.Business;
2323
using OJS.Services.Mentor.Models;
24+
using OJS.Services.Ui.Business;
2425
using OJS.Services.Ui.Data;
2526
using OpenAI;
2627
using OpenAI.Chat;
@@ -29,42 +30,23 @@
2930
using Table = DocumentFormat.OpenXml.Wordprocessing.Table;
3031
using static OJS.Common.GlobalConstants.Settings;
3132

32-
public class MentorBusinessService : IMentorBusinessService
33+
public class MentorBusinessService(
34+
IDataService<UserMentor> userMentorData,
35+
IDataService<MentorPromptTemplate> mentorPromptTemplateData,
36+
IHttpClientFactory httpClientFactory,
37+
IDataService<Setting> settingData,
38+
IContestsDataService contestsData,
39+
ICacheService cache,
40+
ILogger<MentorBusinessService> logger,
41+
OpenAIClient openAiClient,
42+
IProblemResourcesBusinessService problemResourcesBusinessService)
43+
: IMentorBusinessService
3344
{
3445
private const string DocumentNotFoundOrEmpty = "Judge was unable to find the problem's description. Please contact an administrator and report the problem.";
3546

36-
private readonly IDataService<UserMentor> userMentorData;
37-
private readonly IDataService<MentorPromptTemplate> mentorPromptTemplateData;
38-
private readonly IHttpClientFactory httpClientFactory;
39-
private readonly IDataService<Setting> settingData;
40-
private readonly IContestsDataService contestsData;
41-
private readonly ICacheService cache;
42-
private readonly ILogger<MentorBusinessService> logger;
43-
private readonly OpenAIClient openAiClient;
44-
45-
public MentorBusinessService(
46-
IDataService<UserMentor> userMentorData,
47-
IDataService<MentorPromptTemplate> mentorPromptTemplateData,
48-
IHttpClientFactory httpClientFactory,
49-
IDataService<Setting> settingData,
50-
IContestsDataService contestsData,
51-
ICacheService cache,
52-
ILogger<MentorBusinessService> logger,
53-
OpenAIClient openAiClient)
54-
{
55-
this.userMentorData = userMentorData;
56-
this.mentorPromptTemplateData = mentorPromptTemplateData;
57-
this.httpClientFactory = httpClientFactory;
58-
this.settingData = settingData;
59-
this.contestsData = contestsData;
60-
this.cache = cache;
61-
this.logger = logger;
62-
this.openAiClient = openAiClient;
63-
}
64-
6547
public async Task<ConversationResponseModel> StartConversation(ConversationRequestModel model)
6648
{
67-
var settings = await this.settingData
49+
var settings = await settingData
6850
.GetQuery(s => s.Name.Contains(Mentor))
6951
.AsNoTracking()
7052
.ToDictionaryAsync(k => k.Name, v => v.Value);
@@ -81,7 +63,7 @@ public async Task<ConversationResponseModel> StartConversation(ConversationReque
8163
throw new BusinessServiceException($"Your message exceeds the {maxUserInputLength}-character limit. Please shorten it.");
8264
}
8365

84-
var userMentor = await this.userMentorData
66+
var userMentor = await userMentorData
8567
.GetQuery(um => um.Id == model.UserId)
8668
.FirstOrDefaultAsync();
8769

@@ -98,8 +80,8 @@ public async Task<ConversationResponseModel> StartConversation(ConversationReque
9880
QuotaResetTime = DateTime.UtcNow.AddMinutes(GetNumericValue(settings, nameof(MentorQuotaResetTimeInMinutes))),
9981
};
10082

101-
await this.userMentorData.Add(userMentor);
102-
await this.userMentorData.SaveChanges();
83+
await userMentorData.Add(userMentor);
84+
await userMentorData.SaveChanges();
10385
}
10486

10587
if (DateTime.UtcNow >= userMentor.QuotaResetTime)
@@ -127,7 +109,7 @@ public async Task<ConversationResponseModel> StartConversation(ConversationReque
127109

128110
var messagesToSend = new List<ChatMessage>();
129111

130-
var systemMessage = await this.cache.Get(
112+
var systemMessage = await cache.Get(
131113
string.Format(CultureInfo.InvariantCulture, CacheConstants.MentorSystemMessage, model.UserId, model.ProblemId),
132114
async () => await this.GetSystemMessage(model),
133115
CacheConstants.OneHourInSeconds);
@@ -172,7 +154,7 @@ public async Task<ConversationResponseModel> StartConversation(ConversationReque
172154

173155
messagesToSend.AddRange(recentMessages.Select(m => CreateChatMessage(m.Role, m.Content)));
174156

175-
var chat = this.openAiClient.GetChatClient(mentorModel);
157+
var chat = openAiClient.GetChatClient(mentorModel);
176158
var response = await chat.CompleteChatAsync(messagesToSend, new ChatCompletionOptions
177159
{
178160
MaxOutputTokenCount = GetNumericValue(settings, nameof(MentorMaxOutputTokenCount)),
@@ -205,7 +187,7 @@ public async Task<ConversationResponseModel> StartConversation(ConversationReque
205187

206188
userMentor.RequestsMade++;
207189
userMentor.TotalRequestsMade++;
208-
await this.userMentorData.SaveChanges();
190+
await userMentorData.SaveChanges();
209191

210192
return GetResponseModel(model, maxUserInputLength);
211193
}
@@ -283,7 +265,7 @@ private string ExtractSectionFromDocument(
283265
}
284266
catch (Exception)
285267
{
286-
this.logger.LogFileParsingFailure(problemId, contestId);
268+
logger.LogFileParsingFailure(problemId, contestId);
287269
throw new BusinessServiceException(DocumentNotFoundOrEmpty);
288270
}
289271
}
@@ -416,7 +398,7 @@ private void TruncateMessages(
416398
}
417399

418400
// Log the percentage of content truncated
419-
this.logger.LogPercentageOfMessageContentTruncated(percentageTruncated, problemId);
401+
logger.LogPercentageOfMessageContentTruncated(percentageTruncated, problemId);
420402

421403
// Stop if we've removed enough tokens
422404
if (tokensToRemove <= 0)
@@ -429,7 +411,7 @@ private void TruncateMessages(
429411
if (removedMessageCount > 0)
430412
{
431413
// Log the number of messages removed
432-
this.logger.LogTruncatedMentorMessages(initialMessageCount, removedMessageCount, problemId);
414+
logger.LogTruncatedMentorMessages(initialMessageCount, removedMessageCount, problemId);
433415
}
434416
}
435417

@@ -566,12 +548,12 @@ private async Task<ConversationMessageModel> GetSystemMessage(ConversationReques
566548
* .FirstOrDefaultAsync() will always return a template. This
567549
* will be changed in the future.
568550
*/
569-
var template = await this.mentorPromptTemplateData
551+
var template = await mentorPromptTemplateData
570552
.GetQuery()
571553
.AsNoTracking()
572554
.FirstOrDefaultAsync();
573555

574-
var problemsResources = await this.contestsData
556+
var problemsResources = await contestsData
575557
.GetByIdQuery(model.ContestId)
576558
.AsNoTracking()
577559
.Select(c => c.ProblemGroups
@@ -596,7 +578,7 @@ private async Task<ConversationMessageModel> GetSystemMessage(ConversationReques
596578

597579
if (problemsDescription is null)
598580
{
599-
this.logger.LogProblemDescriptionResourceNotFound(model.ProblemId, model.ContestId);
581+
logger.LogProblemDescriptionResourceNotFound(model.ProblemId, model.ContestId);
600582
throw new BusinessServiceException(DocumentNotFoundOrEmpty);
601583
}
602584

@@ -628,12 +610,13 @@ private async Task<ConversationMessageModel> GetSystemMessage(ConversationReques
628610

629611
private async Task<byte[]> DownloadDocument(string link, int problemId, int contestId)
630612
{
613+
link = problemResourcesBusinessService.SafeConvertToSvnLink(link);
631614
using var client = this.CreateClientForLink(link);
632615
var fileBytes = await this.FetchResource(link, client, problemId, contestId);
633616

634617
if (!IsExpectedFormat(fileBytes))
635618
{
636-
this.logger.LogInvalidDocumentFormat(problemId, contestId, link);
619+
logger.LogInvalidDocumentFormat(problemId, contestId, link);
637620
throw new BusinessServiceException(DocumentNotFoundOrEmpty);
638621
}
639622

@@ -648,15 +631,15 @@ private HttpClient CreateClientForLink(string link)
648631
}
649632

650633
// 1) Try the SVN client for links from the SVN server
651-
var svnClient = this.httpClientFactory.CreateClient(ServiceConstants.SvnHttpClientName);
634+
var svnClient = httpClientFactory.CreateClient(ServiceConstants.SvnHttpClientName);
652635
if (svnClient.BaseAddress != null
653636
&& string.Equals(svnClient.BaseAddress.Host, uri.Host, StringComparison.OrdinalIgnoreCase))
654637
{
655638
return svnClient;
656639
}
657640

658641
// 2) Fallback: use the default client
659-
return this.httpClientFactory.CreateClient(ServiceConstants.DefaultHttpClientName);
642+
return httpClientFactory.CreateClient(ServiceConstants.DefaultHttpClientName);
660643
}
661644

662645
private async Task<byte[]> FetchResource(string link, HttpClient client, int problemId, int contestId)
@@ -667,7 +650,7 @@ private async Task<byte[]> FetchResource(string link, HttpClient client, int pro
667650

668651
if (response is not { IsSuccessStatusCode: true })
669652
{
670-
this.logger.LogHttpRequestFailure(
653+
logger.LogHttpRequestFailure(
671654
problemId,
672655
contestId,
673656
response?.StatusCode ?? HttpStatusCode.ServiceUnavailable,
@@ -680,15 +663,15 @@ private async Task<byte[]> FetchResource(string link, HttpClient client, int pro
680663

681664
if (fileBytes.Length == 0)
682665
{
683-
this.logger.LogFileNotFoundOrEmpty(problemId, contestId, link);
666+
logger.LogFileNotFoundOrEmpty(problemId, contestId, link);
684667
throw new BusinessServiceException(DocumentNotFoundOrEmpty);
685668
}
686669

687670
return fileBytes;
688671
}
689672
catch (Exception ex)
690673
{
691-
this.logger.LogResourceDownloadFailure(problemId, contestId, link, ex);
674+
logger.LogResourceDownloadFailure(problemId, contestId, link, ex);
692675
throw new BusinessServiceException(DocumentNotFoundOrEmpty);
693676
}
694677
}

Services/Mentor/OJS.Services.Mentor.Business/OJS.Services.Mentor.Business.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<ProjectReference Include="..\..\UI\OJS.Services.Ui.Business\OJS.Services.Ui.Business.csproj" />
1011
<ProjectReference Include="..\..\UI\OJS.Services.Ui.Data\OJS.Services.Ui.Data.csproj" />
1112
<ProjectReference Include="..\OJS.Services.Mentor.Models\OJS.Services.Mentor.Models.csproj" />
1213
</ItemGroup>

Services/UI/OJS.Services.Ui.Business/IProblemResourcesBusinessService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,11 @@
77
public interface IProblemResourcesBusinessService : IService
88
{
99
Task<ProblemResourceServiceModel> GetResource(int resourceId);
10+
11+
/// <summary>
12+
/// Converts a link to svn link, only if it is not already svn link and the link is a valid alternative, otherwise returns the same link.
13+
/// </summary>
14+
/// <param name="link">The link to convert.</param>
15+
/// <returns></returns>
16+
string SafeConvertToSvnLink(string link);
1017
}

Services/UI/OJS.Services.Ui.Business/Implementations/ProblemResourcesBusinessService.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,66 @@
22

33
using System.Threading.Tasks;
44
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Options;
7+
using OJS.Services.Infrastructure.Configurations;
8+
using OJS.Services.Infrastructure.Constants;
59
using OJS.Services.Infrastructure.Exceptions;
610
using OJS.Services.Ui.Data;
711
using OJS.Services.Ui.Models.Problems;
812
using OJS.Services.Infrastructure.Extensions;
13+
using System;
14+
using System.Linq;
915

10-
public class ProblemResourcesBusinessService(IProblemResourcesDataService problemResourcesDataService)
16+
public class ProblemResourcesBusinessService(
17+
IProblemResourcesDataService problemResourcesDataService,
18+
IOptions<SvnConfig> svnConfigAccessor,
19+
ILogger<ProblemResourcesBusinessService> logger)
1120
: IProblemResourcesBusinessService
1221
{
22+
private readonly SvnConfig svnConfig = svnConfigAccessor.Value;
23+
1324
public async Task<ProblemResourceServiceModel> GetResource(int resourceId)
1425
=> await problemResourcesDataService
1526
.GetByIdQuery(resourceId)
1627
.AsNoTracking()
1728
.MapCollection<ProblemResourceServiceModel>()
1829
.FirstOrDefaultAsync()
1930
?? throw new BusinessServiceException($"Problem resource with ID {resourceId} not found.");
31+
32+
public string SafeConvertToSvnLink(string link)
33+
{
34+
if (!Uri.TryCreate(link, UriKind.Absolute, out var incomingUri))
35+
{
36+
// Not a valid link
37+
return link;
38+
}
39+
40+
if (!Uri.TryCreate(this.svnConfig.BaseUrl, UriKind.Absolute, out var svnUri))
41+
{
42+
logger.LogSvnBaseUrlNotValid(this.svnConfig.BaseUrl);
43+
return link;
44+
}
45+
46+
var alternativeUri = this.svnConfig.AlternativeBaseUrls?
47+
.Select(raw => Uri.TryCreate(raw, UriKind.Absolute, out var u) ? u : null)
48+
.FirstOrDefault(u => u != null && u.IsBaseOf(incomingUri));
49+
50+
if (alternativeUri == null)
51+
{
52+
logger.LogAlternativeBaseUrlNotFoundForLink(link);
53+
return link;
54+
}
55+
56+
// Get the part of the path that comes after the alternative base.
57+
var relativeUri = alternativeUri.MakeRelativeUri(incomingUri);
58+
59+
var newUri = new Uri(svnUri, relativeUri);
60+
61+
var newLink = newUri.ToString();
62+
63+
logger.LogLinkSuccessfullyConvertedToSvnLink(link, newLink);
64+
65+
return newLink;
66+
}
2067
}

0 commit comments

Comments
 (0)