diff --git a/docs/api/rest/dicom-association.md b/docs/api/rest/dicom-association.md new file mode 100644 index 000000000..1b2ffc5a2 --- /dev/null +++ b/docs/api/rest/dicom-association.md @@ -0,0 +1,59 @@ + + +# DICOM Association information + +The `/dicom-associations' endpoint is for retrieving a list of information regarding dicom +associations. + +## GET /dicom-associations/ + +#### Query Parameters + +| Name | Type | Description | +|------------|----------|---------------------------------------------| +| startTime | DateTime | (Optional) Start date to query from. | +| endTime | DateTime | (Optional) End date to query from. | +| pageNumber | Number | (Optional) Page number to query.(default 0) | +| pageSize | Number | (Optional) Page size of query. | + +Max & Defaults for PageSize can be set in appSettings. + +```json +"endpointSettings": { + "defaultPageSize": number, + "maxPageSize": number +} +``` + +Endpoint returns a paged result for example + +```json +{ + "PageNumber": 1, + "PageSize": 10, + "FirstPage": "/payload?pageNumber=1&pageSize=10", + "LastPage": "/payload?pageNumber=1&pageSize=10", + "TotalPages": 1, + "TotalRecords": 3, + "NextPage": null, + "PreviousPage": null, + "Data": [...] + "Succeeded": true, + "Errors": null, + "Message": null +} +``` diff --git a/src/Configuration/HttpEndpointSettings.cs b/src/Configuration/HttpEndpointSettings.cs new file mode 100644 index 000000000..d62a045a6 --- /dev/null +++ b/src/Configuration/HttpEndpointSettings.cs @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.InformaticsGateway.Configuration +{ + public class HttpEndpointSettings + { + [ConfigurationKeyName("defaultPageSize")] + public int DefaultPageSize { get; set; } = 10; + + [ConfigurationKeyName("maxPageSize")] + public int MaxPageSize { get; set; } = 10; + } +} diff --git a/src/Configuration/InformaticsGatewayConfiguration.cs b/src/Configuration/InformaticsGatewayConfiguration.cs index e87229134..b7bfd77d2 100644 --- a/src/Configuration/InformaticsGatewayConfiguration.cs +++ b/src/Configuration/InformaticsGatewayConfiguration.cs @@ -82,6 +82,7 @@ public class InformaticsGatewayConfiguration [ConfigurationKeyName("plugins")] public PlugInConfiguration PlugInConfigurations { get; set; } + public InformaticsGatewayConfiguration() { Dicom = new DicomConfiguration(); diff --git a/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs b/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs index 5ca178cd2..2457dcb7e 100644 --- a/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs +++ b/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs @@ -23,5 +23,20 @@ public interface IDicomAssociationInfoRepository Task> ToListAsync(CancellationToken cancellationToken = default); Task AddAsync(DicomAssociationInfo item, CancellationToken cancellationToken = default); + + /// + /// Retrieves a list of DicomAssociationInfo in the database. + /// + Task> GetAllAsync(int skip, + int? limit, + DateTime startTime, + DateTime endTime, + CancellationToken cancellationToken); + + /// + /// Gets count of objects + /// + /// Count of objects. + Task CountAsync(); } } diff --git a/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs b/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs index d381e58fc..fce537511 100644 --- a/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs +++ b/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs @@ -67,6 +67,24 @@ public async Task AddAsync(DicomAssociationInfo item, Canc }).ConfigureAwait(false); } + public async Task> GetAllAsync(int skip, + int? limit, + DateTime startTime, + DateTime endTime, + CancellationToken cancellationToken) + { + return await _dataset + .Where(t => + t.DateTimeDisconnected >= startTime.ToUniversalTime() && + t.DateTimeDisconnected <= endTime.ToUniversalTime()) + .Skip(skip) + .Take(limit!.Value) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + + public Task CountAsync() => _dataset.LongCountAsync(); + public async Task> ToListAsync(CancellationToken cancellationToken = default) { return await _retryPolicy.ExecuteAsync(async () => diff --git a/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs b/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs index 8ed336cd8..3696b8e48 100644 --- a/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs @@ -59,6 +59,27 @@ public DicomAssociationInfoRepositoryTest(SqliteDatabaseFixture databaseFixture) _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); } + [Fact] + public async Task GivenDestinationApplicationEntitiesInTheDatabase_WhenGetAllAsyncCalled_ExpectLimitedEntitiesToBeReturned() + { + var store = new DicomAssociationInfoRepository(_serviceScopeFactory.Object, _logger.Object, _options); + var startTime = DateTime.Now; + var endTime = DateTime.MinValue; + var filter = new Func(t => + t.DateTimeDisconnected >= startTime.ToUniversalTime() && + t.DateTimeDisconnected <= endTime.ToUniversalTime()); + + var expected = _databaseFixture.DatabaseContext.Set() + .Where(filter) + .Skip(0) + .Take(1) + .ToList(); + var actual = await store.GetAllAsync(0, 1, startTime, endTime, default).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + [Fact] public async Task GivenADicomAssociationInfo_WhenAddingToDatabase_ExpectItToBeSaved() { diff --git a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs index 6f1a83c6d..7fd23164b 100644 --- a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs @@ -89,6 +89,25 @@ public async Task GivenADicomAssociationInfo_WhenAddingToDatabase_ExpectItToBeSa actual!.DateTimeDisconnected.Should().BeCloseTo(association.DateTimeDisconnected, TimeSpan.FromMilliseconds(500)); } + [Fact] + public async Task GivenDestinationApplicationEntitiesInTheDatabase_WhenGetAllAsyncCalled_ExpectLimitedEntitiesToBeReturned() + { + var store = new DicomAssociationInfoRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var collection = _databaseFixture.Database.GetCollection(nameof(DicomAssociationInfo)); + var startTime = DateTime.Now; + var endTime = DateTime.MinValue; + var builder = Builders.Filter; + var filter = builder.Empty; + filter &= builder.Where(t => t.DateTimeDisconnected >= startTime.ToUniversalTime()); + filter &= builder.Where(t => t.DateTimeDisconnected <= endTime.ToUniversalTime()); + var expected = await collection.Find(filter).ToListAsync().ConfigureAwait(false); + var actual = await store.GetAllAsync(0, 1, startTime, endTime, default).ConfigureAwait(false); + + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected, options => options.Excluding(p => p.DateTimeCreated)); + } + [Fact] public async Task GivenDestinationApplicationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned() { diff --git a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs index bc376631b..395f8f4cf 100755 --- a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs +++ b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs @@ -29,7 +29,7 @@ namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories { - public class DicomAssociationInfoRepository : IDicomAssociationInfoRepository, IDisposable + public class DicomAssociationInfoRepository : MongoDBRepositoryBase, IDicomAssociationInfoRepository, IDisposable { private readonly ILogger _logger; private readonly IServiceScope _scope; @@ -78,6 +78,29 @@ public async Task> ToListAsync(CancellationToken canc }).ConfigureAwait(false); } + public Task> GetAllAsync(int skip, + int? limit, + DateTime startTime, + DateTime endTime, + CancellationToken cancellationToken) + { + var builder = Builders.Filter; + var filter = builder.Empty; + filter &= builder.Where(t => t.DateTimeDisconnected >= startTime.ToUniversalTime()); + filter &= builder.Where(t => t.DateTimeDisconnected <= endTime.ToUniversalTime()); + + return GetAllAsync(_collection, + filter, + Builders.Sort.Descending(x => x.DateTimeDisconnected), + skip, + limit); + } + + public Task CountAsync() + { + return _collection.CountDocumentsAsync(Builders.Filter.Empty); + } + protected virtual void Dispose(bool disposing) { if (!_disposedValue) diff --git a/src/Database/MongoDB/Repositories/MongoDBRepositoryBase.cs b/src/Database/MongoDB/Repositories/MongoDBRepositoryBase.cs new file mode 100644 index 000000000..06f81a9d0 --- /dev/null +++ b/src/Database/MongoDB/Repositories/MongoDBRepositoryBase.cs @@ -0,0 +1,64 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using MongoDB.Driver; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories +{ + public abstract class MongoDBRepositoryBase + { + /// + /// Get All T that match filters provided. + /// + /// + /// Collection to run against. + /// Filter function you can filter on properties of T. + /// Function used to sort data. + /// Items to skip. + /// Items to limit results by. + /// + protected static async Task> GetAllAsync(IMongoCollection collection, + Expression>? filterFunction, + SortDefinition sortFunction, + int? skip = null, + int? limit = null) + { + return await collection + .Find(filterFunction) + .Skip(skip) + .Limit(limit) + .Sort(sortFunction) + .ToListAsync().ConfigureAwait(false); + } + + protected static async Task> GetAllAsync(IMongoCollection collection, + FilterDefinition filterFunction, + SortDefinition sortFunction, + int? skip = null, + int? limit = null) + { + var result = await collection + .Find(filterFunction) + .Skip(skip) + .Limit(limit) + .Sort(sortFunction) + .ToListAsync().ConfigureAwait(false); + return result; + } + } +} diff --git a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs index a494082c4..48fcdd325 100644 --- a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs +++ b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs @@ -173,5 +173,11 @@ public static partial class Log [LoggerMessage(EventId = 8204, Level = LogLevel.Error, Message = "Failed to store FHIR resource.")] public static partial void ErrorStoringFhirResource(this ILogger logger, Exception ex); + + // + // Dicom Associations Controller. + // + [LoggerMessage(EventId = 8300, Level = LogLevel.Error, Message = "Unexpected error occurred in GET /dicom-associations API..")] + public static partial void DicomAssociationsControllerGetError(this ILogger logger, Exception ex); } } diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs index 9c3c95336..d0fa5d970 100644 --- a/src/InformaticsGateway/Program.cs +++ b/src/InformaticsGateway/Program.cs @@ -97,6 +97,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureServices((hostContext, services) => { services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway")); + services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:httpEndpointSettings")); services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:messaging")); services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:storage")); services.AddOptions().Bind(hostContext.Configuration.GetSection("MonaiDeployAuthentication")); diff --git a/src/InformaticsGateway/Services/Common/Pagination/PagedResponse.cs b/src/InformaticsGateway/Services/Common/Pagination/PagedResponse.cs new file mode 100644 index 000000000..00fa1d355 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/Pagination/PagedResponse.cs @@ -0,0 +1,107 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using Monai.Deploy.InformaticsGateway.Services.UriService; + +namespace Monai.Deploy.InformaticsGateway.Services.Common.Pagination +{ + /// + /// Paged Response for use with pagination's. + /// + /// Type of response. + public class PagedResponse : Response + { + /// + /// Initializes a new instance of the class. + /// + /// Response Data. + /// Page number. + /// Page size. + public PagedResponse(T data, int pageNumber, int pageSize) + { + PageNumber = pageNumber; + PageSize = pageSize; + Data = data; + Message = null; + Succeeded = true; + Errors = null; + } + + /// + /// Gets or sets PageNumber. + /// + public int PageNumber { get; set; } + + /// + /// Gets or sets PageSize. + /// + public int PageSize { get; set; } + + /// + /// Gets or sets FirstPage. + /// + public string? FirstPage { get; set; } + + /// + /// Gets or sets LastPage. + /// + public string? LastPage { get; set; } + + /// + /// Gets or sets TotalPages. + /// + public int TotalPages { get; set; } + + /// + /// Gets or sets TotalRecords. + /// + public long TotalRecords { get; set; } + + /// + /// Gets or sets NextPage. + /// + public string? NextPage { get; set; } + + /// + /// Gets or sets previousPage. + /// + public string? PreviousPage { get; set; } + + public void SetUp(PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) + { + var totalPages = (double)totalRecords / PageSize; + var roundedTotalPages = Convert.ToInt32(Math.Ceiling(totalPages)); + + var pageNumber = validFilter.PageNumber ?? 0; + NextPage = + pageNumber >= 1 && pageNumber < roundedTotalPages + ? uriService.GetPageUriString(new PaginationFilter(pageNumber + 1, PageSize), route) + : null; + + PreviousPage = + pageNumber - 1 >= 1 && pageNumber <= roundedTotalPages + ? uriService.GetPageUriString(new PaginationFilter(pageNumber - 1, PageSize), route) + : null; + + FirstPage = uriService.GetPageUriString(new PaginationFilter(1, PageSize), route); + LastPage = uriService.GetPageUriString(new PaginationFilter(roundedTotalPages, PageSize), route); + TotalPages = roundedTotalPages; + TotalRecords = totalRecords; + } + } +} diff --git a/src/InformaticsGateway/Services/Common/Pagination/PaginationFilter.cs b/src/InformaticsGateway/Services/Common/Pagination/PaginationFilter.cs new file mode 100644 index 000000000..9183c2005 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/Pagination/PaginationFilter.cs @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Monai.Deploy.InformaticsGateway.Services.Common.Pagination +{ + /// + /// Pagination Filter class. + /// + public class PaginationFilter + { + /// + /// Initializes a new instance of the class. + /// + public PaginationFilter() + { + PageNumber = 1; + PageSize = null; + } + + /// + /// Initializes a new instance of the class. + /// + /// Page size with limit set in the config. + /// Page size 1 or above. + /// Max page size. + public PaginationFilter(int pageNumber, int pageSize, int maxPageSize = 10) + { + PageNumber = pageNumber < 1 ? 1 : pageNumber; + PageSize = pageSize > maxPageSize ? maxPageSize : pageSize; + } + + /// + /// Gets or sets page number. + /// + public int? PageNumber { get; set; } + + /// + /// Gets or sets page size. + /// + public int? PageSize { get; set; } + + public int GetSkip() => (PageNumber - 1) * PageSize ?? 0; + } +} diff --git a/src/InformaticsGateway/Services/Common/Pagination/Response.cs b/src/InformaticsGateway/Services/Common/Pagination/Response.cs new file mode 100644 index 000000000..ba0bec7b9 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/Pagination/Response.cs @@ -0,0 +1,67 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace Monai.Deploy.InformaticsGateway.Services.Common.Pagination +{ + /// + /// Response object. + /// + /// Type of response data. + public class Response + { + /// + /// Initializes a new instance of the class. + /// + public Response() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Response data. + public Response(T data) + { + Succeeded = true; + Message = string.Empty; + Errors = Array.Empty(); + Data = data; + } + + /// + /// Gets or sets Data. + /// + public T? Data { get; set; } + + /// + /// Gets or sets a value indicating whether response has succeeded. + /// + public bool Succeeded { get; set; } + + /// + /// Gets or sets errors. + /// + public string[]? Errors { get; set; } = Array.Empty(); + + /// + /// Gets or sets message. + /// + public string? Message { get; set; } + } +} diff --git a/src/InformaticsGateway/Services/Common/Pagination/TimeFilter.cs b/src/InformaticsGateway/Services/Common/Pagination/TimeFilter.cs new file mode 100644 index 000000000..20f09e962 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/Pagination/TimeFilter.cs @@ -0,0 +1,51 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace Monai.Deploy.InformaticsGateway.Services.Common.Pagination +{ + public class TimeFilter : PaginationFilter + { + public TimeFilter() + { + } + + public TimeFilter(DateTime? startTime, + DateTime? endTime, + int pageNumber, + int pageSize, + int maxPageSize) : base(pageNumber, + pageSize, + maxPageSize) + { + if (endTime == default) + { + EndTime = DateTime.Now; + } + + if (startTime == default) + { + StartTime = new DateTime(2023, 1, 1); + } + } + + public DateTime? StartTime { get; set; } + + public DateTime? EndTime { get; set; } + } +} diff --git a/src/InformaticsGateway/Services/Http/ApiControllerBase.cs b/src/InformaticsGateway/Services/Http/ApiControllerBase.cs new file mode 100644 index 000000000..6ecd5050f --- /dev/null +++ b/src/InformaticsGateway/Services/Http/ApiControllerBase.cs @@ -0,0 +1,51 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net; +using Microsoft.AspNetCore.Mvc; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + /// + /// Base Api Controller. + /// + [ApiController] + public class ApiControllerBase : ControllerBase + { + /// + /// Initializes a new instance of the class. + /// + public ApiControllerBase() + { + } + + /// + /// Gets internal Server Error 500. + /// + public static int InternalServerError => (int)HttpStatusCode.InternalServerError; + + /// + /// Gets bad Request 400. + /// + public static new int BadRequest => (int)HttpStatusCode.BadRequest; + + /// + /// Gets notFound 404. + /// + public static new int NotFound => (int)HttpStatusCode.NotFound; + } +} diff --git a/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs b/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs new file mode 100644 index 000000000..d794cecff --- /dev/null +++ b/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs @@ -0,0 +1,91 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; +using Monai.Deploy.InformaticsGateway.Services.UriService; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + [Route("dicom-associations")] + public class DicomAssociationInfoController : PagedApiControllerBase + { + private const string Endpoint = "/dicom-associations"; + private readonly ILogger _logger; + private readonly IDicomAssociationInfoRepository _dicomRepo; + private readonly IUriService _uriService; + + public DicomAssociationInfoController(ILogger logger, + IOptions options, + IDicomAssociationInfoRepository dicomRepo, + IUriService uriService) : base(options) + { + _logger = logger; + _dicomRepo = dicomRepo; + _uriService = uriService; + } + + /// + /// Gets a paged response list of all workflows. + /// + /// Filters. + /// paged response of subset of all workflows. + [HttpGet] + [ProducesResponseType(typeof(PagedResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task GetAllAsync([FromQuery] TimeFilter filter) + { + try + { + var route = Request?.Path.Value ?? string.Empty; + var pageSize = filter.PageSize ?? EndpointOptions.Value.DefaultPageSize; + var validFilter = new TimeFilter( + filter.StartTime, + filter.EndTime, + filter.PageNumber ?? 0, + pageSize, + EndpointOptions.Value.MaxPageSize); + + var pagedData = await _dicomRepo.GetAllAsync( + validFilter.GetSkip(), + validFilter.PageSize, + filter.StartTime!.Value, + filter.EndTime!.Value, default).ConfigureAwait(false); + + var dataTotal = await _dicomRepo.CountAsync().ConfigureAwait(false); + var pagedResponse = CreatePagedResponse(pagedData.ToList(), validFilter, dataTotal, _uriService, route); + return Ok(pagedResponse); + } + catch (Exception e) + { + _logger.DicomAssociationsControllerGetError(e); + return Problem($"Unexpected error occurred: {e.Message}", Endpoint, InternalServerError); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Http/PagedApiControllerBase.cs b/src/InformaticsGateway/Services/Http/PagedApiControllerBase.cs new file mode 100644 index 000000000..09db23ea7 --- /dev/null +++ b/src/InformaticsGateway/Services/Http/PagedApiControllerBase.cs @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using Ardalis.GuardClauses; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; +using Monai.Deploy.InformaticsGateway.Services.UriService; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + public class PagedApiControllerBase : ApiControllerBase + { + protected readonly IOptions EndpointOptions; + + public PagedApiControllerBase(IOptions options) + { + EndpointOptions = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Creates a pagination paged response. + /// + /// Data set type. + /// Data set. + /// Filters. + /// Total records. + /// Uri service. + /// Route. + /// Returns . + public PagedResponse> CreatePagedResponse(IEnumerable pagedData, PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) + { + Guard.Against.Null(pagedData, nameof(pagedData)); + Guard.Against.Null(validFilter, nameof(validFilter)); + Guard.Against.Null(route, nameof(route)); + Guard.Against.Null(uriService, nameof(uriService)); + + var pageSize = validFilter.PageSize ?? EndpointOptions.Value.DefaultPageSize; + var response = new PagedResponse>(pagedData, validFilter.PageNumber ?? 0, pageSize); + + response.SetUp(validFilter, totalRecords, uriService, route); + return response; + } + } +} diff --git a/src/InformaticsGateway/Services/UriService/IUriService.cs b/src/InformaticsGateway/Services/UriService/IUriService.cs new file mode 100644 index 000000000..6fc38e20e --- /dev/null +++ b/src/InformaticsGateway/Services/UriService/IUriService.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; + +namespace Monai.Deploy.InformaticsGateway.Services.UriService +{ + /// + /// Uri Service. + /// + public interface IUriService + { + /// + /// Gets Relative Uri path with filters as a string. + /// + /// Filters. + /// Route. + /// Relative Uri string. + public string GetPageUriString(PaginationFilter filter, string route); + } +} diff --git a/src/InformaticsGateway/Services/UriService/UriService.cs b/src/InformaticsGateway/Services/UriService/UriService.cs new file mode 100644 index 000000000..28f78488c --- /dev/null +++ b/src/InformaticsGateway/Services/UriService/UriService.cs @@ -0,0 +1,60 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using Microsoft.AspNetCore.WebUtilities; +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; + +namespace Monai.Deploy.InformaticsGateway.Services.UriService +{ + /// + /// Uri Service. + /// + public class UriService : IUriService + { + private readonly Uri _baseUri; + + /// + /// Initializes a new instance of the class. + /// + /// Base Url. + public UriService(Uri baseUri) + { + _baseUri = baseUri; + } + + /// + /// Gets page uri. + /// + /// Filters. + /// Route. + /// Uri. + public string GetPageUriString(PaginationFilter filter, string route) + { + if (_baseUri.ToString().EndsWith('/') && route.StartsWith('/')) + { + route = route.TrimStart('/'); + } + + var endpointUri = new Uri(string.Concat(_baseUri, route)); + var modifiedUri = QueryHelpers.AddQueryString(endpointUri.ToString(), "pageNumber", filter.PageNumber.ToString()!); + modifiedUri = QueryHelpers.AddQueryString(modifiedUri, "pageSize", filter?.PageSize?.ToString() ?? string.Empty); + var uri = new Uri(modifiedUri); + return uri.IsAbsoluteUri ? uri.PathAndQuery : uri.OriginalString; + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Common/Pagination/PagedResponseTest.cs b/src/InformaticsGateway/Test/Services/Common/Pagination/PagedResponseTest.cs new file mode 100644 index 000000000..1d3ed888f --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/Pagination/PagedResponseTest.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; +using Monai.Deploy.InformaticsGateway.Services.UriService; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common.Pagination +{ + public class PagedResponseTest + { + [Fact] + public void SetUp_GivenExpectedInput_ReturnsExpectedResult() + { + var filter = new PaginationFilter(); + var data = new List { "orange", "apple", "donkey" }; + var pagedResponse = new PagedResponse>(data,0, 3); + var uriService = new UriService(new Uri("https://test.com")); + pagedResponse.SetUp(filter, 9, uriService, "test"); + + Assert.Equal(pagedResponse.FirstPage, "/test?pageNumber=1&pageSize=3"); + Assert.Equal(pagedResponse.LastPage, "/test?pageNumber=3&pageSize=3"); + Assert.Equal(pagedResponse.NextPage, "/test?pageNumber=2&pageSize=3"); + Assert.Null(pagedResponse.PreviousPage); + + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs new file mode 100644 index 000000000..1558ec691 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs @@ -0,0 +1,80 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; +using Monai.Deploy.InformaticsGateway.Services.Http; +using Monai.Deploy.InformaticsGateway.Services.UriService; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Http +{ + public class DicomAssociationInfoControllerTest + { + private readonly Mock> _logger; + private Mock _loggerFactory; + private readonly DicomAssociationInfoController _controller; + private readonly IOptions _options; + private readonly Mock _repo; + private readonly UriService _uriService; + + public DicomAssociationInfoControllerTest() + { + _loggerFactory = new Mock(); + _logger = new Mock>(); + _repo = new Mock(); + _loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_logger.Object); + _options = Options.Create(new HttpEndpointSettings()); + _uriService = new UriService(new Uri("https://test.com/")); + + _controller = new DicomAssociationInfoController(_logger.Object, _options, _repo.Object, _uriService); + } + + [Fact] + public async Task GetAllAsync_GiveExpectedInput_ReturnsOK() + { + var input = new TimeFilter + { + EndTime = DateTime.Now, + StartTime = DateTime.MinValue, + PageNumber = 0, + PageSize = 1 + }; + _repo.Setup(r => r.GetAllAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + var result = await _controller.GetAllAsync(input); + + var okResult = Assert.IsType(result); + var response = Assert.IsType>>(okResult.Value); + Assert.Equal(0 ,response.TotalRecords); + Assert.Empty(response.Data); + } + } +}