mirror of https://github.com/abpframework/abp.git
csharpabpc-sharpframeworkblazoraspnet-coredotnet-coreaspnetcorearchitecturesaasdomain-driven-designangularmulti-tenancy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
19 KiB
480 lines
19 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Caching.Distributed;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Volo.Abp;
|
|
using Volo.Abp.Caching;
|
|
using Volo.Docs.Caching;
|
|
using Volo.Docs.Documents.FullSearch.Elastic;
|
|
using Volo.Docs.Projects;
|
|
using Volo.Extensions;
|
|
|
|
namespace Volo.Docs.Documents
|
|
{
|
|
public class DocumentAppService : DocsAppServiceBase, IDocumentAppService
|
|
{
|
|
public INavigationTreePostProcessor NavigationTreePostProcessor { get; set; }
|
|
|
|
private readonly IProjectRepository _projectRepository;
|
|
private readonly IDocumentRepository _documentRepository;
|
|
private readonly IDocumentSourceFactory _documentStoreFactory;
|
|
protected IDistributedCache<DocumentResource> ResourceCache { get; }
|
|
protected IDistributedCache<DocumentUpdateInfo> DocumentUpdateCache { get; }
|
|
protected IHostEnvironment HostEnvironment { get; }
|
|
private readonly IDocumentFullSearch _documentFullSearch;
|
|
private readonly DocsElasticSearchOptions _docsElasticSearchOptions;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly TimeSpan _cacheTimeout;
|
|
private readonly TimeSpan _documentResourceAbsoluteExpiration;
|
|
private readonly TimeSpan _documentResourceSlidingExpiration;
|
|
|
|
public DocumentAppService(
|
|
IProjectRepository projectRepository,
|
|
IDocumentRepository documentRepository,
|
|
IDocumentSourceFactory documentStoreFactory,
|
|
IDistributedCache<DocumentResource> resourceCache,
|
|
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
|
|
IHostEnvironment hostEnvironment,
|
|
IDocumentFullSearch documentFullSearch,
|
|
IOptions<DocsElasticSearchOptions> docsElasticSearchOptions,
|
|
IConfiguration configuration)
|
|
{
|
|
_projectRepository = projectRepository;
|
|
_documentRepository = documentRepository;
|
|
_documentStoreFactory = documentStoreFactory;
|
|
ResourceCache = resourceCache;
|
|
DocumentUpdateCache = documentUpdateCache;
|
|
HostEnvironment = hostEnvironment;
|
|
_documentFullSearch = documentFullSearch;
|
|
_configuration = configuration;
|
|
_docsElasticSearchOptions = docsElasticSearchOptions.Value;
|
|
_cacheTimeout = GetCacheTimeout();
|
|
_documentResourceAbsoluteExpiration = GetDocumentResourceAbsoluteExpirationTimeout();
|
|
_documentResourceSlidingExpiration = GetDocumentResourceSlidingExpirationTimeout();
|
|
|
|
NavigationTreePostProcessor = NullNavigationTreePostProcessor.Instance;
|
|
}
|
|
|
|
public virtual async Task<DocumentWithDetailsDto> GetAsync(GetDocumentInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
var inputVersionStringBuilder = new StringBuilder();
|
|
input.Version = inputVersionStringBuilder.Append(GetProjectVersionPrefixIfExist(project)).Append(input.Version).ToString();
|
|
|
|
return await GetDocumentWithDetailsDtoAsync(
|
|
project,
|
|
input.Name,
|
|
input.LanguageCode,
|
|
input.Version
|
|
);
|
|
}
|
|
|
|
public virtual async Task<DocumentWithDetailsDto> GetDefaultAsync(GetDefaultDocumentInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
var sb = new StringBuilder();
|
|
input.Version = sb.Append(GetProjectVersionPrefixIfExist(project)).Append(input.Version).ToString();
|
|
|
|
sb.Clear();
|
|
|
|
return await GetDocumentWithDetailsDtoAsync(
|
|
project,
|
|
sb.Append(project.DefaultDocumentName).Append(".").Append(project.Format).ToString(),
|
|
input.LanguageCode,
|
|
input.Version
|
|
);
|
|
}
|
|
|
|
public virtual async Task<NavigationNode> GetNavigationAsync(GetNavigationDocumentInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
input.Version = GetProjectVersionPrefixIfExist(project) + input.Version;
|
|
|
|
var navigationDocument = await GetDocumentWithDetailsDtoAsync(
|
|
project,
|
|
project.NavigationDocumentName,
|
|
input.LanguageCode,
|
|
input.Version
|
|
);
|
|
|
|
if (!DocsJsonSerializerHelper.TryDeserialize<NavigationNode>(navigationDocument.Content,
|
|
out var navigationNode))
|
|
{
|
|
throw new UserFriendlyException(
|
|
$"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}.");
|
|
}
|
|
|
|
var leafs = navigationNode.Items.GetAllNodes(x => x.Items)
|
|
.Where(x => !x.Path.IsNullOrWhiteSpace())
|
|
.ToList();
|
|
|
|
foreach (var leaf in leafs)
|
|
{
|
|
var cacheKey =
|
|
CacheKeyGenerator.GenerateDocumentUpdateInfoCacheKey(project, leaf.Path, input.LanguageCode,
|
|
input.Version);
|
|
var documentUpdateInfo = await DocumentUpdateCache.GetAsync(cacheKey);
|
|
if (documentUpdateInfo != null)
|
|
{
|
|
leaf.CreationTime = documentUpdateInfo.CreationTime;
|
|
leaf.LastUpdatedTime = documentUpdateInfo.LastUpdatedTime;
|
|
leaf.LastSignificantUpdateTime = documentUpdateInfo.LastSignificantUpdateTime;
|
|
}
|
|
}
|
|
|
|
await NavigationTreePostProcessor.ProcessAsync(
|
|
new NavigationTreePostProcessorContext(
|
|
navigationDocument,
|
|
navigationNode
|
|
)
|
|
);
|
|
|
|
return navigationNode;
|
|
}
|
|
|
|
public virtual async Task<DocumentResourceDto> GetResourceAsync(GetDocumentResourceInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
input.Version = string.IsNullOrWhiteSpace(input.Version) ? project.LatestVersionBranchName : input.Version;
|
|
input.Version = GetProjectVersionPrefixIfExist(project) + input.Version;
|
|
|
|
var cacheKey =
|
|
CacheKeyGenerator.GenerateDocumentResourceCacheKey(project, input.Name, input.LanguageCode,
|
|
input.Version);
|
|
|
|
async Task<DocumentResource> GetResourceAsync()
|
|
{
|
|
var source = _documentStoreFactory.Create(project.DocumentStoreType);
|
|
return await source.GetResource(project, input.Name, input.LanguageCode, input.Version);
|
|
}
|
|
|
|
if (HostEnvironment.IsDevelopment())
|
|
{
|
|
return ObjectMapper.Map<DocumentResource, DocumentResourceDto>(await GetResourceAsync());
|
|
}
|
|
|
|
return ObjectMapper.Map<DocumentResource, DocumentResourceDto>(
|
|
await ResourceCache.GetOrAddAsync(
|
|
cacheKey,
|
|
GetResourceAsync,
|
|
() => new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = _documentResourceAbsoluteExpiration,
|
|
SlidingExpiration = _documentResourceSlidingExpiration
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
public virtual async Task<List<DocumentSearchOutput>> SearchAsync(DocumentSearchInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
input.Version = GetProjectVersionPrefixIfExist(project) + input.Version;
|
|
|
|
var esDocs =
|
|
await _documentFullSearch.SearchAsync(input.Context, project.Id, input.LanguageCode, input.Version);
|
|
|
|
return esDocs.Select(esDoc => new DocumentSearchOutput //TODO: auto map
|
|
{
|
|
Name = esDoc.Name,
|
|
FileName = esDoc.FileName,
|
|
Version = esDoc.Version,
|
|
LanguageCode = esDoc.LanguageCode,
|
|
Highlight = esDoc.Highlight
|
|
}).Where(x =>
|
|
x.FileName != project.NavigationDocumentName && x.FileName != project.ParametersDocumentName)
|
|
.ToList();
|
|
}
|
|
|
|
public virtual async Task<bool> FullSearchEnabledAsync()
|
|
{
|
|
return await Task.FromResult(_docsElasticSearchOptions.Enable);
|
|
}
|
|
|
|
public virtual async Task<List<string>> GetUrlsAsync(string prefix)
|
|
{
|
|
var documentUrls = new List<string>();
|
|
var projects = await _projectRepository.GetListAsync();
|
|
|
|
foreach (var project in projects)
|
|
{
|
|
var documentWithoutDetailsList = await _documentRepository.GetListWithoutDetailsByProjectId(project.Id);
|
|
|
|
foreach (var document in documentWithoutDetailsList)
|
|
{
|
|
try
|
|
{
|
|
await AddDocumentToUrls(prefix, project, document, documentUrls);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogException(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
return documentUrls;
|
|
}
|
|
|
|
private async Task AddDocumentToUrls(string prefix, Project project, DocumentWithoutDetails document,
|
|
List<string> documentUrls)
|
|
{
|
|
var navigationNodes = await GetNavigationNodesAsync(project, document);
|
|
AddDocumentUrls(prefix, navigationNodes, documentUrls, project, document);
|
|
}
|
|
|
|
private void AddDocumentUrls(string prefix,
|
|
List<NavigationNode> navigationNodes,
|
|
List<string> documentUrls,
|
|
Project project,
|
|
DocumentWithoutDetails document)
|
|
{
|
|
navigationNodes?.ForEach(node =>
|
|
{
|
|
documentUrls.AddIfNotContains(
|
|
GetDocumentLinks(node, documentUrls, prefix, project.ShortName, document)
|
|
);
|
|
});
|
|
}
|
|
|
|
private async Task<List<NavigationNode>> GetNavigationNodesAsync(Project project,
|
|
DocumentWithoutDetails document)
|
|
{
|
|
var version = GetProjectVersionPrefixIfExist(project) + document.Version;
|
|
var navigationDocument = await GetDocumentWithDetailsDtoAsync(
|
|
project,
|
|
project.NavigationDocumentName,
|
|
document.LanguageCode,
|
|
version
|
|
);
|
|
|
|
if (!DocsJsonSerializerHelper.TryDeserialize<NavigationNode>(navigationDocument.Content,
|
|
out var navigationNode))
|
|
{
|
|
throw new UserFriendlyException(
|
|
$"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}.");
|
|
}
|
|
|
|
return navigationNode.Items;
|
|
}
|
|
|
|
private List<string> GetDocumentLinks(NavigationNode node, List<string> documentUrls, string prefix,
|
|
string shortName, DocumentWithoutDetails document)
|
|
{
|
|
if (!IsExternalLink(node.Path))
|
|
{
|
|
documentUrls.AddIfNotContains(
|
|
NormalizePath(prefix, node.Path, shortName, document)
|
|
);
|
|
}
|
|
|
|
node.Items?.ForEach(childNode =>
|
|
{
|
|
GetDocumentLinks(childNode, documentUrls, prefix, shortName, document);
|
|
});
|
|
|
|
return documentUrls;
|
|
}
|
|
|
|
private string NormalizePath(string prefix, string path, string shortName, DocumentWithoutDetails document)
|
|
{
|
|
var pathWithoutFileExtension = RemoveFileExtensionFromPath(path, document.Format);
|
|
|
|
var normalizedPathStringBuilder = new StringBuilder();
|
|
normalizedPathStringBuilder.Append(prefix).Append(document.LanguageCode).Append("/").Append(shortName)
|
|
.Append("/").Append(document.Version).Append("/").Append(pathWithoutFileExtension);
|
|
|
|
return normalizedPathStringBuilder.ToString();
|
|
}
|
|
|
|
private string RemoveFileExtensionFromPath(string path, string format)
|
|
{
|
|
if (path == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return path.EndsWith("." + format)
|
|
? path.Left(path.Length - format.Length - 1)
|
|
: path;
|
|
}
|
|
|
|
private static bool IsExternalLink(string path)
|
|
{
|
|
if (path.IsNullOrEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public virtual async Task<DocumentParametersDto> GetParametersAsync(GetParametersDocumentInput input)
|
|
{
|
|
var project = await _projectRepository.GetAsync(input.ProjectId);
|
|
|
|
input.Version = GetProjectVersionPrefixIfExist(project) + input.Version;
|
|
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(project.ParametersDocumentName))
|
|
{
|
|
return await Task.FromResult<DocumentParametersDto>(null);
|
|
}
|
|
|
|
var document = await GetDocumentWithDetailsDtoAsync(
|
|
project,
|
|
project.ParametersDocumentName,
|
|
input.LanguageCode,
|
|
input.Version
|
|
);
|
|
|
|
if (!DocsJsonSerializerHelper.TryDeserialize<DocumentParametersDto>(document.Content,
|
|
out var documentParameters))
|
|
{
|
|
throw new UserFriendlyException(
|
|
$"Cannot validate document parameters file '{project.ParametersDocumentName}' for the project {project.Name}.");
|
|
}
|
|
|
|
return documentParameters;
|
|
}
|
|
catch (DocumentNotFoundException)
|
|
{
|
|
Logger.LogWarning($"Parameter file ({project.ParametersDocumentName}) not found!");
|
|
return new DocumentParametersDto();
|
|
}
|
|
}
|
|
|
|
protected virtual async Task<DocumentWithDetailsDto> GetDocumentWithDetailsDtoAsync(
|
|
Project project,
|
|
string documentName,
|
|
string languageCode,
|
|
string version)
|
|
{
|
|
version = string.IsNullOrWhiteSpace(version) ? project.LatestVersionBranchName : version;
|
|
|
|
if (HostEnvironment.IsDevelopment())
|
|
{
|
|
return await GetDocumentAsync(documentName, project, languageCode, version);
|
|
}
|
|
|
|
var document = await _documentRepository.FindAsync(project.Id, documentName, languageCode, version);
|
|
if (document == null)
|
|
{
|
|
return await GetDocumentAsync(documentName, project, languageCode, version);
|
|
}
|
|
|
|
if (document.LastCachedTime + _cacheTimeout < DateTime.Now)
|
|
{
|
|
return await GetDocumentAsync(documentName, project, languageCode, version, document);
|
|
}
|
|
|
|
return CreateDocumentWithDetailsDto(project, document);
|
|
}
|
|
|
|
protected virtual DocumentWithDetailsDto CreateDocumentWithDetailsDto(Project project, Document document)
|
|
{
|
|
var documentDto = ObjectMapper.Map<Document, DocumentWithDetailsDto>(document);
|
|
|
|
documentDto.Project = ObjectMapper.Map<Project, ProjectDto>(project);
|
|
documentDto.Contributors =
|
|
ObjectMapper.Map<List<DocumentContributor>, List<DocumentContributorDto>>(document.Contributors);
|
|
|
|
return documentDto;
|
|
}
|
|
|
|
private async Task<DocumentWithDetailsDto> GetDocumentAsync(string documentName, Project project,
|
|
string languageCode, string version, Document oldDocument = null)
|
|
{
|
|
Logger.LogInformation($"Not found in the cache. Requesting {documentName} from the source...");
|
|
|
|
var source = _documentStoreFactory.Create(project.DocumentStoreType);
|
|
var sourceDocument = await source.GetDocumentAsync(project, documentName, languageCode, version,
|
|
oldDocument?.LastSignificantUpdateTime);
|
|
|
|
await _documentRepository.DeleteAsync(project.Id, sourceDocument.Name, sourceDocument.LanguageCode, sourceDocument.Version, autoSave: true);
|
|
await _documentRepository.InsertAsync(sourceDocument, true);
|
|
|
|
Logger.LogInformation($"Document retrieved: {documentName}");
|
|
|
|
var cacheKey = CacheKeyGenerator.GenerateDocumentUpdateInfoCacheKey(
|
|
project,
|
|
sourceDocument.Name,
|
|
sourceDocument.LanguageCode,
|
|
sourceDocument.Version
|
|
);
|
|
|
|
await DocumentUpdateCache.SetAsync(cacheKey, new DocumentUpdateInfo
|
|
{
|
|
Name = sourceDocument.Name,
|
|
CreationTime = sourceDocument.CreationTime,
|
|
LastUpdatedTime = sourceDocument.LastUpdatedTime,
|
|
LastSignificantUpdateTime = sourceDocument.LastSignificantUpdateTime
|
|
});
|
|
|
|
return CreateDocumentWithDetailsDto(project, sourceDocument);
|
|
}
|
|
|
|
private TimeSpan GetCacheTimeout()
|
|
{
|
|
var value = _configuration["Volo.Docs:DocumentCacheTimeoutInterval"];
|
|
if (value.IsNullOrEmpty())
|
|
{
|
|
return TimeSpan.FromHours(6);
|
|
}
|
|
|
|
return TimeSpan.Parse(value);
|
|
}
|
|
|
|
private TimeSpan GetDocumentResourceAbsoluteExpirationTimeout()
|
|
{
|
|
var value = _configuration["Volo.Docs:DocumentResource.AbsoluteExpirationRelativeToNow"];
|
|
if (value.IsNullOrEmpty())
|
|
{
|
|
return TimeSpan.FromHours(6);
|
|
}
|
|
|
|
return TimeSpan.Parse(value);
|
|
}
|
|
|
|
private TimeSpan GetDocumentResourceSlidingExpirationTimeout()
|
|
{
|
|
var value = _configuration["Volo.Docs:DocumentResource.SlidingExpiration"];
|
|
if (value.IsNullOrEmpty())
|
|
{
|
|
return TimeSpan.FromMinutes(30);
|
|
}
|
|
|
|
return TimeSpan.Parse(value);
|
|
}
|
|
|
|
private string GetProjectVersionPrefixIfExist(Project project)
|
|
{
|
|
if (GetGithubVersionProviderSource(project) != GithubVersionProviderSource.Branches)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return project.ExtraProperties["VersionBranchPrefix"].ToString();
|
|
}
|
|
|
|
private GithubVersionProviderSource GetGithubVersionProviderSource(Project project)
|
|
{
|
|
return project.ExtraProperties.ContainsKey("GithubVersionProviderSource")
|
|
? (GithubVersionProviderSource) (long) project.ExtraProperties["GithubVersionProviderSource"]
|
|
: GithubVersionProviderSource.Releases;
|
|
}
|
|
}
|
|
}
|