diff --git a/.env.example b/.env.example index 50a5a4e..c18cc14 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -JWT_KEY=xxx \ No newline at end of file +JWT_KEY=PLACEHOLDER_REPLACE_WITH_STRONG_KEY_MIN_32_CHARS_BEFORE_USE + +REDIS_URL=redis:6379 +REDIS_PORT=6379 diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template_api.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template_api.md index 3606bb1..897711a 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template_api.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template_api.md @@ -2,7 +2,7 @@ ### Pre-requisites -- [ ] I have gone through the Contributing guidelines for [Submitting a Pull Request (PR)](https://github.com/OsmosysSoftware/document-service/blob/main/CONTRIBUTING.md#submitting-a-pull-request-pr) and ensured that this is not a duplicate PR. +- [ ] I have gone through the Contributing guidelines for [Submitting a Pull Request (PR)](https://github.com/OsmosysSoftware/osmodoc/blob/main/CONTRIBUTING.md#submitting-a-pull-request-pr) and ensured that this is not a duplicate PR. - [ ] I have performed unit testing for the new feature added or updated to ensure the new features added are working as expected. - [ ] I have performed preliminary testing to ensure that any existing features are not impacted and any new features are working as expected as a whole. - [ ] I have added/updated the `.env.example` file with the required values as applicable. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2433ab4..0002c1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ -# Contributing to DocumentService -Welcome to the document-service project! We appreciate your interest in contributing to the project and making it even better. As a contributor, +# Contributing to OsmoDoc +Welcome to the osmodoc project! We appreciate your interest in contributing to the project and making it even better. As a contributor, please follow the guidelines outlined below: ## Table of contents @@ -13,8 +13,8 @@ please follow the guidelines outlined below: ## Got a question or problem? **If you have questions or encounter problems, please refrain from opening issues for general support questions**. GitHub issues are primarily for bug -reports and feature requests. For general questions and support, consider using [Stack Overflow](https://stackoverflow.com/questions/tagged/document-service) -and tag your questions with the `document-service` tag. Here's why Stack Overflow is a preferred platform: +reports and feature requests. For general questions and support, consider using [Stack Overflow](https://stackoverflow.com/questions/tagged/osmodoc) +and tag your questions with the `osmodoc` tag. Here's why Stack Overflow is a preferred platform: - Questions and answers are publicly available, helping others. - The voting system on Stack Overflow highlights the best answers. @@ -23,8 +23,8 @@ To save time for both you and us, we will close issues related to general suppor ## Found any issues and bugs -If you find a bug in the source code, you can help us by [submitting an issue](https://github.com/OsmosysSoftware/document-service/issues/new) -to our GitHub Repository. Even better, you can submit a [pull request](https://github.com/OsmosysSoftware/document-service/pulls) with a fix. +If you find a bug in the source code, you can help us by [submitting an issue](https://github.com/OsmosysSoftware/osmodoc/issues/new) +to our GitHub Repository. Even better, you can submit a [pull request](https://github.com/OsmosysSoftware/osmodoc/pulls) with a fix. ## Submission guidelines @@ -34,19 +34,19 @@ Before you submit an issue, please check the issue tracker to see if a similar i For us to address and fix a bug, we need to reproduce it. Thus when submitting a bug report, we will ask for a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Providing a live, reproducible scenario helps us understand the issue better. Information to include: -- The version of the document-service you are using. +- The version of the osmodoc you are using. - Any third-party libraries and their versions. - A use-case that demonstrates the issue. Without a minimal reproduction, we may need to close the issue due to insufficient information. -You can file new issues using our [new issue form](https://github.com/OsmosysSoftware/document-service/issues/new). +You can file new issues using our [new issue form](https://github.com/OsmosysSoftware/osmodoc/issues/new). ### Submitting a pull request (PR) Before submitting a Pull Request (PR), please follow these guidelines: -1. Search GitHub [pull requests](https://github.com/OsmosysSoftware/document-service/pulls) to ensure there is no open or closed PR +1. Search GitHub [pull requests](https://github.com/OsmosysSoftware/osmodoc/pulls) to ensure there is no open or closed PR related to your submission. 2. Fork this repository. 3. Make your changes in a new Git branch. @@ -63,12 +63,12 @@ Before submitting a Pull Request (PR), please follow these guidelines: ```shell git push origin my-fix-branch ``` -7. Send a pull request to the `document-service:main`. +7. Send a pull request to the `osmodoc:main`. -- **If we suggest changes, then:** - - Make the required updates. - - Ensure that your changes do not break existing functionality or introduce new issues. - - Rebase your branch and force push to your GitHub repository. This will update your Pull Request. +- **If we suggest changes, then:** + - � Make the required updates. + - � Ensure that your changes do not break existing functionality or introduce new issues. + - � Rebase your branch and force push to your GitHub repository. This will update your Pull Request. That's it! Thank you for your contribution! @@ -90,7 +90,7 @@ To ensure consistency throughout the source code, follow these rules as you work ## Commit message guidelines In this project, we have specific rules for formatting our Git commit messages. These guidelines result in more readable messages that are easy -to follow when reviewing the project's history. Additionally, we use these commit messages to **generate the document-service change log**. +to follow when reviewing the project's history. Additionally, we use these commit messages to **generate the osmodoc change log**. ### Commit message format @@ -110,7 +110,7 @@ on GitHub as well as in various git tools. Footer should contain a [closing reference to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if any. -Samples: (even more [samples](https://github.com/OsmosysSoftware/document-service/commits/main)) +Samples: (even more [samples](https://github.com/OsmosysSoftware/osmodoc/commits/main)) `docs: update change log to beta.5` `fix: need to depend on latest rxjs and zone.js` diff --git a/Dockerfile b/Dockerfile index 8deee30..e4f3a8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,19 +10,19 @@ EXPOSE 5000 ENV BUILD_CONFIGURATION=Debug # Copy data -COPY ["DocumentService.API/DocumentService.API.csproj", "DocumentService.API/"] -COPY ["DocumentService/DocumentService.csproj", "DocumentService/"] +COPY ["OsmoDoc.API/OsmoDoc.API.csproj", "OsmoDoc.API/"] +COPY ["OsmoDoc/OsmoDoc.csproj", "OsmoDoc/"] # Restore the project dependencies -RUN dotnet restore "./DocumentService.API/./DocumentService.API.csproj" -RUN dotnet restore "./DocumentService/./DocumentService.csproj" +RUN dotnet restore "./OsmoDoc.API/./OsmoDoc.API.csproj" +RUN dotnet restore "./OsmoDoc/./OsmoDoc.csproj" # Copy the rest of the data COPY . . -WORKDIR "/app/DocumentService.API" +WORKDIR "/app/OsmoDoc.API" # Build the project and store artifacts in /out folder -RUN dotnet publish "./DocumentService.API.csproj" -c BUILD_CONFIGURATION -o /app/out +RUN dotnet publish "./OsmoDoc.API.csproj" -c BUILD_CONFIGURATION -o /app/out # Use the official ASP.NET runtime image as the base image FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base @@ -45,4 +45,4 @@ RUN chmod 755 /usr/bin/wkhtmltopdf RUN npm install -g --only=prod ejs # Set the entry point for the container -ENTRYPOINT ["dotnet", "DocumentService.API.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "OsmoDoc.API.dll"] \ No newline at end of file diff --git a/DocumentService/DocumentService.csproj b/DocumentService/DocumentService.csproj deleted file mode 100644 index 0bb92df..0000000 --- a/DocumentService/DocumentService.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net8.0 - True - - - - - - - - - diff --git a/DocumentService/Pdf/Models/ContentMetaData.cs b/DocumentService/Pdf/Models/ContentMetaData.cs deleted file mode 100644 index 1f9cb3e..0000000 --- a/DocumentService/Pdf/Models/ContentMetaData.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DocumentService.Pdf.Models -{ - public class ContentMetaData - { - public string Placeholder { get; set; } - public string Content { get; set; } - } -} - - diff --git a/DocumentService/Pdf/Models/DocumentData.cs b/DocumentService/Pdf/Models/DocumentData.cs deleted file mode 100644 index 9a93c7a..0000000 --- a/DocumentService/Pdf/Models/DocumentData.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocumentService.Pdf.Models -{ - public class DocumentData - { - public List Placeholders { get; set; } - } -} diff --git a/DocumentService/Pdf/PdfDocumentGenerator.cs b/DocumentService/Pdf/PdfDocumentGenerator.cs deleted file mode 100644 index a3cb308..0000000 --- a/DocumentService/Pdf/PdfDocumentGenerator.cs +++ /dev/null @@ -1,213 +0,0 @@ -using DocumentService.Pdf.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace DocumentService.Pdf -{ - public class PdfDocumentGenerator - { - public static void GeneratePdf(string toolFolderAbsolutePath, string templatePath, List metaDataList, string outputFilePath, bool isEjsTemplate, string serializedEjsDataJson) - { - try - { - if (!File.Exists(templatePath)) - { - throw new Exception("The file path you provided is not valid."); - } - - if (isEjsTemplate) - { - // Validate if template in file path is an ejs file - if (Path.GetExtension(templatePath).ToLower() != ".ejs") - { - throw new Exception("Input template should be a valid EJS file"); - } - - // Convert ejs file to an equivalent html - templatePath = ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); - } - - // Modify html template with content data and generate pdf - string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); - ConvertHtmlToPdf(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); - - if (isEjsTemplate) - { - // If input template was an ejs file, then the template path contains path to html converted from ejs - if (Path.GetExtension(templatePath).ToLower() == ".html") - { - // If template path contains path to converted html template then delete it - File.Delete(templatePath); - } - } - } - catch (Exception e) - { - throw e; - } - } - - private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) - { - string htmlContent = File.ReadAllText(templatePath); - - foreach (ContentMetaData metaData in metaDataList) - { - htmlContent = htmlContent.Replace($"{{{metaData.Placeholder}}}", metaData.Content); - } - - string directoryPath = Path.GetDirectoryName(outputFilePath); - string tempHtmlFilePath = Path.Combine(directoryPath, "Modified"); - string tempHtmlFile = Path.Combine(tempHtmlFilePath, "modifiedHtml.html"); - - if (!Directory.Exists(tempHtmlFilePath)) - { - Directory.CreateDirectory(tempHtmlFilePath); - } - - File.WriteAllText(tempHtmlFile, htmlContent); - return tempHtmlFile; - } - - private static void ConvertHtmlToPdf(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) - { - string wkHtmlToPdfPath = "cmd.exe"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - wkHtmlToPdfPath = "wkhtmltopdf"; - } - - /* - * FIXME: Issue if tools file path has spaces in between - */ - string arguments = HtmlToPdfArgumentsBasedOnOS(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); - - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = wkHtmlToPdfPath, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = psi; - process.Start(); - process.WaitForExit(); - string output = process.StandardOutput.ReadToEnd(); - string errors = process.StandardError.ReadToEnd(); - } - - File.Delete(modifiedHtmlFilePath); - } - - private static string ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string ejsDataJson) - { - // Generate directory - string directoryPath = Path.GetDirectoryName(outputFilePath); - string tempDirectoryFilePath = Path.Combine(directoryPath, "Temp"); - - if (!Directory.Exists(tempDirectoryFilePath)) - { - Directory.CreateDirectory(tempDirectoryFilePath); - } - - // Generate file path to converted html template - string tempHtmlFilePath = Path.Combine(tempDirectoryFilePath, "htmlTemplate.html"); - - // If the ejs data json is invalid then throw exception - if (!string.IsNullOrWhiteSpace(ejsDataJson) && !IsValidJSON(ejsDataJson)) - { - throw new Exception("Received invalid JSON data for EJS template"); - } - - // Write json data string to json file - string ejsDataJsonFilePath = Path.Combine(tempDirectoryFilePath, "ejsData.json"); - File.WriteAllText(ejsDataJsonFilePath, ejsDataJson); - - string commandLine = "cmd.exe"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - commandLine = "ejs"; - } - string arguments = EjsToHtmlArgumentsBasedOnOS(ejsFilePath, ejsDataJsonFilePath, tempHtmlFilePath); - - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = commandLine, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = psi; - process.Start(); - process.WaitForExit(); - string output = process.StandardOutput.ReadToEnd(); - string errors = process.StandardError.ReadToEnd(); - } - - // Delete json data file - File.Delete(ejsDataJsonFilePath); - - return tempHtmlFilePath; - } - - private static bool IsValidJSON(string json) - { - try - { - JToken.Parse(json); - return true; - } - catch (JsonReaderException) - { - return false; - } - } - - private static string HtmlToPdfArgumentsBasedOnOS(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return $"/C {toolFolderAbsolutePath} \"{modifiedHtmlFilePath}\" \"{outputFilePath}\""; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return $"{modifiedHtmlFilePath} {outputFilePath}"; - } - else - { - throw new Exception("Unknown operating system"); - } - } - - private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejsDataJsonFilePath, string tempHtmlFilePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return $"/C ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return $"{ejsFilePath} -f {ejsDataJsonFilePath} -o {tempHtmlFilePath}"; - } - else - { - throw new Exception("Unknown operating system"); - } - } - } -} diff --git a/DocumentService/Word/Models/ContentData.cs b/DocumentService/Word/Models/ContentData.cs deleted file mode 100644 index c6f4ae2..0000000 --- a/DocumentService/Word/Models/ContentData.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a content placeholder in a Word document. - /// - public class ContentData - { - /// - /// Gets or sets the placeholder name. - /// - public string Placeholder { get; set; } - - /// - /// Gets or sets the content to replace the placeholder with. - /// - public string Content { get; set; } - - /// - /// Gets or sets the content type of the placeholder (text or image). - /// - public ContentType ContentType { get; set; } - - /// - /// Gets or sets the parent body of the placeholder (none or table). - /// - - public ParentBody ParentBody { get; set; } - } -} diff --git a/DocumentService/Word/Models/DocumentData.cs b/DocumentService/Word/Models/DocumentData.cs deleted file mode 100644 index 1ca80fe..0000000 --- a/DocumentService/Word/Models/DocumentData.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a Word document, including content placeholders and table data. - /// - public class DocumentData - { - /// - /// Gets or sets the list of content placeholders in the document. - /// - public List Placeholders { get; set; } - - /// - /// Gets or sets the list of table data in the document. - /// - - public List TablesData { get; set; } - } -} diff --git a/DocumentService/Word/Models/Enums.cs b/DocumentService/Word/Models/Enums.cs deleted file mode 100644 index dff7e01..0000000 --- a/DocumentService/Word/Models/Enums.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace DocumentService.Word.Models -{ - - /// - /// Represents the content type of a placeholder in a Word document. - /// - public enum ContentType - { - /// - /// The placeholder represents text content. - /// - Text = 0, - - /// - /// The placeholder represents an image. - /// - Image = 1 - } - - /// - /// Represents the parent body of a placeholder in a Word document. - /// - public enum ParentBody - { - /// - /// The placeholder does not have a parent body. - /// - None = 0, - - /// - /// The placeholder belongs to a table. - /// - - Table = 1 - } -} diff --git a/DocumentService/Word/Models/TableData.cs b/DocumentService/Word/Models/TableData.cs deleted file mode 100644 index f5e8a2a..0000000 --- a/DocumentService/Word/Models/TableData.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a table in a Word document. - /// - public class TableData - { - /// - /// Gets or sets the position of the table in the document. - /// - public int TablePos { get; set; } - - /// - /// Gets or sets the list of dictionaries representing the data for each row in the table. - /// Each dictionary contains column header-value pairs. - /// - - public List> Data { get; set; } - } -} diff --git a/DocumentService/Word/WordDocumentGenerator.cs b/DocumentService/Word/WordDocumentGenerator.cs deleted file mode 100644 index 659d55e..0000000 --- a/DocumentService/Word/WordDocumentGenerator.cs +++ /dev/null @@ -1,310 +0,0 @@ -using DocumentFormat.OpenXml.Drawing; -using DocumentFormat.OpenXml.Drawing.Wordprocessing; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Wordprocessing; -using DocumentService.Word.Models; -using NPOI.XWPF.UserModel; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; - -namespace DocumentService.Word -{ - /// - /// Provides functionality to generate Word documents based on templates and data. - /// - public static class WordDocumentGenerator - { - /// - /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. - /// - /// The file path of the template document. - /// The data to replace the placeholders in the template. - /// The file path to save the generated document. - public static void GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) - { - try - { - List contentData = documentData.Placeholders; - List tablesData = documentData.TablesData; - - // Creating dictionaries for each type of placeholders - Dictionary textPlaceholders = new Dictionary(); - Dictionary tableContentPlaceholders = new Dictionary(); - Dictionary imagePlaceholders = new Dictionary(); - - foreach (ContentData content in contentData) - { - if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - textPlaceholders.Add(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) - { - string placeholder = content.Placeholder; - imagePlaceholders.Add(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - tableContentPlaceholders.Add(placeholder, content.Content); - } - } - - // Create document of the template - XWPFDocument document = GetXWPFDocument(templateFilePath); - - // For each element in the document - foreach (IBodyElement element in document.BodyElements) - { - if (element.ElementType == BodyElementType.PARAGRAPH) - { - // If element is a paragraph - XWPFParagraph paragraph = (XWPFParagraph)element; - - // If the paragraph is empty string or the placeholder regex does not match then continue - if (paragraph.ParagraphText == string.Empty || !new Regex(@"{[a-zA-Z]+}").IsMatch(paragraph.ParagraphText)) - { - continue; - } - - // Replace placeholders in paragraph with values - paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); - } - else if (element.ElementType == BodyElementType.TABLE) - { - // If element is a table - XWPFTable table = (XWPFTable)element; - - // Replace placeholders in a table - table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); - - // Populate the table with data if it is passed in tablesData list - foreach (TableData insertData in tablesData) - { - if (insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) - { - table = PopulateTable(table, insertData); - } - } - } - } - - // Write the document to output file path and close the document - WriteDocument(document, outputFilePath); - document.Close(); - - /* - * Image Replacement is done after writing the document here, - * because for Text Replacement, NPOI package is being used - * and for Image Replacement, OpeXML package is used. - * Since both the packages have different execution method, so they are handled separately - */ - // Replace all the image placeholders in the output file - ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); - } - catch (Exception ex) - { - throw ex; - } - } - - /// - /// Retrieves an instance of XWPFDocument from the specified document file path. - /// - /// The file path of the Word document. - /// An instance of XWPFDocument representing the Word document. - private static XWPFDocument GetXWPFDocument(string docFilePath) - { - FileStream readStream = File.OpenRead(docFilePath); - XWPFDocument document = new XWPFDocument(readStream); - readStream.Close(); - return document; - } - - /// - /// Writes the XWPFDocument to the specified file path. - /// - /// The XWPFDocument to write. - /// The file path to save the document. - private static void WriteDocument(XWPFDocument document, string filePath) - { - using (FileStream writeStream = File.Create(filePath)) - { - document.Write(writeStream); - } - } - - /// - /// Replaces the text placeholders in a paragraph with the specified values. - /// - /// The XWPFParagraph containing the placeholders. - /// The dictionary of text placeholders and their corresponding values. - /// The updated XWPFParagraph. - private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) - { - // Get a list of all placeholders in the current paragraph - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") - .Cast() - .Select(s => s.Groups[0].Value).ToList(); - - // For each placeholder in paragraph - foreach (string placeholder in placeholdersTobeReplaced) - { - // Replace text placeholders in paragraph with values - if (textPlaceholders.ContainsKey(placeholder)) - { - paragraph.ReplaceText(placeholder, textPlaceholders[placeholder]); - } - - paragraph.SpacingAfter = 0; - } - - return paragraph; - } - - /// - /// Replaces the text placeholders in a table with the specified values. - /// - /// The XWPFTable containing the placeholders. - /// The dictionary of table content placeholders and their corresponding values. - /// The updated XWPFTable. - private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary tableContentPlaceholders) - { - // Loop through each cell of the table - foreach (XWPFTableRow row in table.Rows) - { - foreach (XWPFTableCell cell in row.GetTableCells()) - { - foreach (XWPFParagraph paragraph in cell.Paragraphs) - { - // Get a list of all placeholders in the current cell - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") - .Cast() - .Select(s => s.Groups[0].Value).ToList(); - - // For each placeholder in the cell - foreach (string placeholder in placeholdersTobeReplaced) - { - // replace the placeholder with its value - if (tableContentPlaceholders.ContainsKey(placeholder)) - { - paragraph.ReplaceText(placeholder, tableContentPlaceholders[placeholder]); - } - } - } - } - } - - return table; - } - - /// - /// Populates a table with the specified data. - /// - /// The XWPFTable to populate. - /// The data to populate the table. - /// The updated XWPFTable. - private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) - { - // Get the header row - XWPFTableRow headerRow = table.GetRow(0); - - // Return if no header row found or if it does not have any column - if (headerRow == null || headerRow.GetTableCells() == null || headerRow.GetTableCells().Count <= 0) - { - return table; - } - - // For each row's data stored in table data - foreach (Dictionary rowData in tableData.Data) - { - // Create a new row and its columns - XWPFTableRow row = table.CreateRow(); - - // For each cell in row - for (int cellNumber = 0; cellNumber < row.GetTableCells().Count; cellNumber++) - { - XWPFTableCell cell = row.GetCell(cellNumber); - - // Get the column header of this cell - string columnHeader = headerRow.GetCell(cellNumber).GetText(); - - // Add the cell's value - if (rowData.ContainsKey(columnHeader)) - { - cell.SetText(rowData[columnHeader]); - } - } - } - - return table; - } - - /// - /// Replaces the image placeholders in the output file with the specified images. - /// - /// The input file path containing the image placeholders. - /// The output file path where the updated document will be saved. - /// The dictionary of image placeholders and their corresponding image paths. - private static void ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) - { - byte[] docBytes = File.ReadAllBytes(inputFilePath); - - // Write document bytes to memory - MemoryStream memoryStream = new MemoryStream(); - memoryStream.Write(docBytes, 0, docBytes.Length); - - using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) - { - MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart; - - // Get a list of drawings (images) - IEnumerable drawings = mainDocumentPart.Document.Descendants().ToList(); - - /* - * FIXME: Look on how we can improve this loop operation. - */ - foreach (Drawing drawing in drawings) - { - DocProperties docProperty = drawing.Descendants().FirstOrDefault(); - - // If drawing / image name is present in imagePlaceholders dictionary, then replace image - if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) - { - List drawingBlips = drawing.Descendants().ToList(); - - foreach (Blip blip in drawingBlips) - { - OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); - - using (BinaryWriter writer = new BinaryWriter(imagePart.GetStream())) - { - string imagePath = imagePlaceholders[docProperty.Name]; - - /* - * WebClient has been deprecated and we need to use HTTPClient. - * This involves the methods to be asynchronous. - */ - using (WebClient webClient = new WebClient()) - { - writer.Write(webClient.DownloadData(imagePath)); - } - } - } - } - } - } - - // Overwrite the output file - FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); - memoryStream.WriteTo(fileStream); - fileStream.Close(); - memoryStream.Close(); - } - } -} \ No newline at end of file diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs new file mode 100644 index 0000000..a27d792 --- /dev/null +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using OsmoDoc.API.Models; +using OsmoDoc.API.Helpers; +using OsmoDoc.Services; + +namespace OsmoDoc.API.Controllers; + +[Route("api")] +[ApiController] +public class LoginController : ControllerBase +{ + private readonly IRedisTokenStoreService _tokenStoreService; + private readonly ILogger _logger; + + public LoginController(IRedisTokenStoreService tokenStoreService, ILogger logger) + { + this._tokenStoreService = tokenStoreService; + this._logger = logger; + } + + [HttpPost] + [Route("login")] + [AllowAnonymous] + public async Task> Login([FromBody] LoginRequestDTO loginRequest) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); + await this._tokenStoreService.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); + + response.Status = ResponseStatus.Success; + response.AuthToken = token; + response.Message = "Token generated successfully"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } + + [HttpPost] + [Route("revoke")] + public async Task> RevokeToken([FromBody] RevokeTokenRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + await this._tokenStoreService.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); + + response.Status = ResponseStatus.Success; + response.Message = "Token revoked"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} \ No newline at end of file diff --git a/DocumentService.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs similarity index 90% rename from DocumentService.API/Controllers/PdfController.cs rename to OsmoDoc.API/Controllers/PdfController.cs index 0413daa..2e11b2a 100644 --- a/DocumentService.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -1,207 +1,193 @@ -using DocumentService.Pdf; -using DocumentService.API.Helpers; -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; - -namespace DocumentService.API.Controllers; - -[Route("api")] -[ApiController] -public class PdfController : ControllerBase -{ - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - - public PdfController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger) - { - this._configuration = configuration; - this._hostingEnvironment = hostingEnvironment; - this._logger = logger; - } - - [HttpPost] - [Authorize] - [Route("pdf/GeneratePdfUsingHtml")] - public async Task> GeneratePdf(PdfGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 html template - string htmlTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value, - CommonMethodsHelper.GenerateRandomFileName("html") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(htmlTemplateFilePath); - - // Save base64 html template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); - - // Initialize tools and output filepaths - string htmlToPDfToolsFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, - CommonMethodsHelper.GenerateRandomFileName("pdf") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Generate and save pdf in output directory - PdfDocumentGenerator.GeneratePdf( - htmlToPDfToolsFilePath, - htmlTemplateFilePath, - request.DocumentData.Placeholders, - outputFilePath, - isEjsTemplate: false, - serializedEjsDataJson: null - ); - - // Convert pdf file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "PDF generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } - - [HttpPost] - [Authorize] - [Route("pdf/GeneratePdfUsingEjs")] - public async Task> GeneratePdfUsingEjs(PdfGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 html template - string ejsTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value, - CommonMethodsHelper.GenerateRandomFileName("ejs") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(ejsTemplateFilePath); - - // Save base64 html template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); - - // Initialize tools and output filepaths - string ejsToPDfToolsFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, - CommonMethodsHelper.GenerateRandomFileName("pdf") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Generate and save pdf in output directory - PdfDocumentGenerator.GeneratePdf( - ejsToPDfToolsFilePath, - ejsTemplateFilePath, - request.DocumentData?.Placeholders, - outputFilePath, - isEjsTemplate: true, - serializedEjsDataJson: request.SerializedEjsDataJson - ); - - // Convert pdf file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "PDF generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } -} +using OsmoDoc.Pdf; +using OsmoDoc.API.Helpers; +using OsmoDoc.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace OsmoDoc.API.Controllers; + +[Route("api")] +[ApiController] +public class PdfController : ControllerBase +{ + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _hostingEnvironment; + private readonly ILogger _logger; + + public PdfController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger) + { + this._configuration = configuration; + this._hostingEnvironment = hostingEnvironment; + this._logger = logger; + } + + [HttpPost] + [Authorize] + [Route("pdf/GeneratePdfUsingHtml")] + public async Task> GeneratePdf(PdfGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 html template + string htmlTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value, + CommonMethodsHelper.GenerateRandomFileName("html") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(htmlTemplateFilePath); + + // Save base64 html template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); + + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + CommonMethodsHelper.GenerateRandomFileName("pdf") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Generate and save pdf in output directory + await PdfDocumentGenerator.GeneratePdf( + htmlTemplateFilePath, + request.DocumentData.Placeholders, + outputFilePath, + isEjsTemplate: false, + serializedEjsDataJson: null + ); + + // Convert pdf file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "PDF generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } + + [HttpPost] + [Authorize] + [Route("pdf/GeneratePdfUsingEjs")] + public async Task> GeneratePdfUsingEjs(PdfGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 html template + string ejsTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value, + CommonMethodsHelper.GenerateRandomFileName("ejs") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(ejsTemplateFilePath); + + // Save base64 html template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); + + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + CommonMethodsHelper.GenerateRandomFileName("pdf") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Generate and save pdf in output directory + await PdfDocumentGenerator.GeneratePdf( + ejsTemplateFilePath, + request.DocumentData?.Placeholders, + outputFilePath, + isEjsTemplate: true, + serializedEjsDataJson: request.SerializedEjsDataJson + ); + + // Convert pdf file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "PDF generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} diff --git a/DocumentService.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs similarity index 94% rename from DocumentService.API/Controllers/WordController.cs rename to OsmoDoc.API/Controllers/WordController.cs index 33601a8..03ca47c 100644 --- a/DocumentService.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -1,156 +1,156 @@ -using AutoMapper; -using DocumentService.Word; -using DocumentService.Word.Models; -using DocumentService.API.Helpers; -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; - -namespace DocumentService.API.Controllers; - -[Route("api")] -[ApiController] -public class WordController : ControllerBase -{ - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public WordController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger, IMapper mapper) - { - this._configuration = configuration; - this._hostingEnvironment = hostingEnvironment; - this._logger = logger; - this._mapper = mapper; - } - - [HttpPost] - [Authorize] - [Route("word/GenerateWordDocument")] - public async Task> GenerateWord(WordGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 docx template - string docxTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - CommonMethodsHelper.GenerateRandomFileName("docx") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(docxTemplateFilePath); - - // Save docx template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, docxTemplateFilePath, this._configuration); - - // Initialize output filepath - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - CommonMethodsHelper.GenerateRandomFileName("docx") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Handle image placeholder data in request - foreach (WordContentDataRequestDTO placeholder in request.DocumentData.Placeholders) - { - if (placeholder.ContentType == ContentType.Image) - { - if (string.IsNullOrWhiteSpace(placeholder.ImageExtension)) - { - throw new BadHttpRequestException("Image extension is required for image content data"); - } - - if (string.IsNullOrWhiteSpace(placeholder.Content)) - { - throw new BadHttpRequestException("Image content data is required"); - } - - // Remove '.' from image extension if present - placeholder.ImageExtension = placeholder.ImageExtension.Replace(".", string.Empty); - - // Generate a random image file name and its path - string imageFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value, - CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(imageFilePath); - - // Save image content base64 string to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(placeholder.Content, imageFilePath, this._configuration); - - // Replace placeholder content with image file path - placeholder.Content = imageFilePath; - } - } - - // Map document data in request to word library model class - DocumentData documentData = new DocumentData - { - Placeholders = this._mapper.Map>(request.DocumentData.Placeholders), - TablesData = request.DocumentData.TablesData - }; - - // Generate and save output docx in output directory - WordDocumentGenerator.GenerateDocumentByTemplate( - docxTemplateFilePath, - documentData, - outputFilePath - ); - - // Convert docx file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "Word document generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } -} +using AutoMapper; +using OsmoDoc.Word; +using OsmoDoc.Word.Models; +using OsmoDoc.API.Helpers; +using OsmoDoc.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace OsmoDoc.API.Controllers; + +[Route("api")] +[ApiController] +public class WordController : ControllerBase +{ + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public WordController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger, IMapper mapper) + { + this._configuration = configuration; + this._hostingEnvironment = hostingEnvironment; + this._logger = logger; + this._mapper = mapper; + } + + [HttpPost] + [Authorize] + [Route("word/GenerateWordDocument")] + public async Task> GenerateWord(WordGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 docx template + string docxTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + CommonMethodsHelper.GenerateRandomFileName("docx") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(docxTemplateFilePath); + + // Save docx template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, docxTemplateFilePath, this._configuration); + + // Initialize output filepath + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + CommonMethodsHelper.GenerateRandomFileName("docx") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Handle image placeholder data in request + foreach (WordContentDataRequestDTO placeholder in request.DocumentData.Placeholders) + { + if (placeholder.ContentType == ContentType.Image) + { + if (string.IsNullOrWhiteSpace(placeholder.ImageExtension)) + { + throw new BadHttpRequestException("Image extension is required for image content data"); + } + + if (string.IsNullOrWhiteSpace(placeholder.Content)) + { + throw new BadHttpRequestException("Image content data is required"); + } + + // Remove '.' from image extension if present + placeholder.ImageExtension = placeholder.ImageExtension.Replace(".", string.Empty); + + // Generate a random image file name and its path + string imageFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value, + CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(imageFilePath); + + // Save image content base64 string to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(placeholder.Content, imageFilePath, this._configuration); + + // Replace placeholder content with image file path + placeholder.Content = imageFilePath; + } + } + + // Map document data in request to word library model class + DocumentData documentData = new DocumentData + { + Placeholders = this._mapper.Map>(request.DocumentData.Placeholders), + TablesData = request.DocumentData.TablesData + }; + + // Generate and save output docx in output directory + await WordDocumentGenerator.GenerateDocumentByTemplate( + docxTemplateFilePath, + documentData, + outputFilePath + ); + + // Convert docx file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "Word document generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} diff --git a/DocumentService.API/DotEnv.cs b/OsmoDoc.API/DotEnv.cs similarity index 93% rename from DocumentService.API/DotEnv.cs rename to OsmoDoc.API/DotEnv.cs index 2b5b4ce..e0801f2 100644 --- a/DocumentService.API/DotEnv.cs +++ b/OsmoDoc.API/DotEnv.cs @@ -1,35 +1,35 @@ -namespace DocumentService.API; - -public static class DotEnv -{ - public static void Load(string filePath) - { - if (!File.Exists(filePath)) - { - return; - } - - foreach (string line in File.ReadAllLines(filePath)) - { - // Check if the line contains '=' - int equalsIndex = line.IndexOf('='); - if (equalsIndex == -1) - { - continue; // Skip lines without '=' - } - - string key = line.Substring(0, equalsIndex).Trim(); - string value = line.Substring(equalsIndex + 1).Trim(); - - // Check if the value starts and ends with double quotation marks - if (value.StartsWith("\"") && value.EndsWith("\"")) - { - // Remove the double quotation marks - value = value[1..^1]; - } - - Environment.SetEnvironmentVariable(key, value); - } - } - -} +namespace OsmoDoc.API; + +public static class DotEnv +{ + public static void Load(string filePath) + { + if (!File.Exists(filePath)) + { + return; + } + + foreach (string line in File.ReadAllLines(filePath)) + { + // Check if the line contains '=' + int equalsIndex = line.IndexOf('='); + if (equalsIndex == -1) + { + continue; // Skip lines without '=' + } + + string key = line.Substring(0, equalsIndex).Trim(); + string value = line.Substring(equalsIndex + 1).Trim(); + + // Check if the value starts and ends with double quotation marks + if (value.StartsWith("\"") && value.EndsWith("\"")) + { + // Remove the double quotation marks + value = value[1..^1]; + } + + Environment.SetEnvironmentVariable(key, value); + } + } + +} diff --git a/DocumentService.API/Helpers/AuthenticationHelper.cs b/OsmoDoc.API/Helpers/AuthenticationHelper.cs similarity index 94% rename from DocumentService.API/Helpers/AuthenticationHelper.cs rename to OsmoDoc.API/Helpers/AuthenticationHelper.cs index ed0dee9..04dc325 100644 --- a/DocumentService.API/Helpers/AuthenticationHelper.cs +++ b/OsmoDoc.API/Helpers/AuthenticationHelper.cs @@ -1,28 +1,28 @@ -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Text; -using System.Security.Claims; - -namespace DocumentService.API.Helpers; - -public class AuthenticationHelper -{ - // Function to generate non expiry jwt token based on email - public static string JwtTokenGenerator(string LoginEmail) - { - SymmetricSecurityKey secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( - Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified") - )); - SigningCredentials signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); - - JwtSecurityToken tokenOptions = new JwtSecurityToken( - claims: new List() - { - new(ClaimTypes.Email, LoginEmail), - }, - signingCredentials: signinCredentials - ); - - return new JwtSecurityTokenHandler().WriteToken(tokenOptions); - } -} +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using System.Security.Claims; + +namespace OsmoDoc.API.Helpers; + +public class AuthenticationHelper +{ + // Function to generate non expiry jwt token based on email + public static string JwtTokenGenerator(string LoginEmail) + { + SymmetricSecurityKey secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( + Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified") + )); + SigningCredentials signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + JwtSecurityToken tokenOptions = new JwtSecurityToken( + claims: new List() + { + new(ClaimTypes.Email, LoginEmail), + }, + signingCredentials: signinCredentials + ); + + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } +} diff --git a/DocumentService.API/Helpers/AutoMappingProfile.cs b/OsmoDoc.API/Helpers/AutoMappingProfile.cs similarity index 62% rename from DocumentService.API/Helpers/AutoMappingProfile.cs rename to OsmoDoc.API/Helpers/AutoMappingProfile.cs index a9cef5c..269ba7b 100644 --- a/DocumentService.API/Helpers/AutoMappingProfile.cs +++ b/OsmoDoc.API/Helpers/AutoMappingProfile.cs @@ -1,13 +1,13 @@ -using AutoMapper; -using DocumentService.Word.Models; -using DocumentService.API.Models; - -namespace DocumentService.API.Helpers; - -public class AutoMappingProfile : Profile -{ - public AutoMappingProfile() - { - this.CreateMap(); - } -} +using AutoMapper; +using OsmoDoc.Word.Models; +using OsmoDoc.API.Models; + +namespace OsmoDoc.API.Helpers; + +public class AutoMappingProfile : Profile +{ + public AutoMappingProfile() + { + this.CreateMap(); + } +} diff --git a/DocumentService.API/Helpers/Base64StringHelper.cs b/OsmoDoc.API/Helpers/Base64StringHelper.cs similarity index 93% rename from DocumentService.API/Helpers/Base64StringHelper.cs rename to OsmoDoc.API/Helpers/Base64StringHelper.cs index 9984908..df48427 100644 --- a/DocumentService.API/Helpers/Base64StringHelper.cs +++ b/OsmoDoc.API/Helpers/Base64StringHelper.cs @@ -1,31 +1,31 @@ -namespace DocumentService.API.Helpers; - -public static class Base64StringHelper -{ - public static async Task SaveBase64StringToFilePath(string base64String, string filePath, IConfiguration configuration) - { - byte[] data = Convert.FromBase64String(base64String); - - long uploadFileSizeLimitBytes = Convert.ToInt64(configuration.GetSection("CONFIG:UPLOAD_FILE_SIZE_LIMIT_BYTES").Value); - - if (data.LongLength > uploadFileSizeLimitBytes) - { - throw new BadHttpRequestException("Uploaded file is too large"); - } - - await File.WriteAllBytesAsync(filePath, data); - } - - public static async Task ConvertFileToBase64String(string filePath) - { - if (File.Exists(filePath)) - { - byte[] fileData = await File.ReadAllBytesAsync(filePath); - return Convert.ToBase64String(fileData); - } - else - { - throw new FileNotFoundException("The file does not exist: " + filePath); - } - } -} +namespace OsmoDoc.API.Helpers; + +public static class Base64StringHelper +{ + public static async Task SaveBase64StringToFilePath(string base64String, string filePath, IConfiguration configuration) + { + byte[] data = Convert.FromBase64String(base64String); + + long uploadFileSizeLimitBytes = Convert.ToInt64(configuration.GetSection("CONFIG:UPLOAD_FILE_SIZE_LIMIT_BYTES").Value); + + if (data.LongLength > uploadFileSizeLimitBytes) + { + throw new BadHttpRequestException("Uploaded file is too large"); + } + + await File.WriteAllBytesAsync(filePath, data); + } + + public static async Task ConvertFileToBase64String(string filePath) + { + if (File.Exists(filePath)) + { + byte[] fileData = await File.ReadAllBytesAsync(filePath); + return Convert.ToBase64String(fileData); + } + else + { + throw new FileNotFoundException("The file does not exist: " + filePath); + } + } +} diff --git a/DocumentService.API/Helpers/CommonMethodsHelper.cs b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs similarity index 93% rename from DocumentService.API/Helpers/CommonMethodsHelper.cs rename to OsmoDoc.API/Helpers/CommonMethodsHelper.cs index dc9006d..fe3de2b 100644 --- a/DocumentService.API/Helpers/CommonMethodsHelper.cs +++ b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs @@ -1,26 +1,26 @@ -namespace DocumentService.API.Helpers; - -public static class CommonMethodsHelper -{ - public static void CreateDirectoryIfNotExists(string filePath) - { - // Get directory name of the file - // If path is a file name only, directory name will be an empty string - string directoryName = Path.GetDirectoryName(filePath); - - if (!string.IsNullOrWhiteSpace(directoryName)) - { - if (!Directory.Exists(directoryName)) - { - // Create all directories on the path that don't already exist - Directory.CreateDirectory(directoryName); - } - } - } - - public static string GenerateRandomFileName(string fileExtension) - { - string randomFileName = Path.GetRandomFileName().Replace(".", string.Empty); - return $"{randomFileName}-{Guid.NewGuid()}.{fileExtension}"; - } -} +namespace OsmoDoc.API.Helpers; + +public static class CommonMethodsHelper +{ + public static void CreateDirectoryIfNotExists(string filePath) + { + // Get directory name of the file + // If path is a file name only, directory name will be an empty string + string directoryName = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrWhiteSpace(directoryName)) + { + if (!Directory.Exists(directoryName)) + { + // Create all directories on the path that don't already exist + Directory.CreateDirectory(directoryName); + } + } + } + + public static string GenerateRandomFileName(string fileExtension) + { + string randomFileName = Path.GetRandomFileName().Replace(".", string.Empty); + return $"{randomFileName}-{Guid.NewGuid()}.{fileExtension}"; + } +} diff --git a/DocumentService.API/Models/BaseResponse.cs b/OsmoDoc.API/Models/BaseResponse.cs similarity index 93% rename from DocumentService.API/Models/BaseResponse.cs rename to OsmoDoc.API/Models/BaseResponse.cs index 0422bbe..a2df3cc 100644 --- a/DocumentService.API/Models/BaseResponse.cs +++ b/OsmoDoc.API/Models/BaseResponse.cs @@ -1,33 +1,33 @@ -using Microsoft.AspNetCore.Mvc; - -namespace DocumentService.API.Models; - -public enum ResponseStatus -{ - Success, - Fail, - Error -} - -public class BaseResponse -{ - public BaseResponse(ResponseStatus status) => this.Status = status; - public ResponseStatus? Status { get; set; } - public string? Base64 { get; set; } - public string? AuthToken { get; set; } - public string? Message { get; set; } - public string? StackTrace { get; set; } -} - -public class ModelValidationBadRequest -{ - public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext) - { - return new BadRequestObjectResult(actionContext.ModelState - .Where(modelError => modelError.Value.Errors.Any()) - .Select(modelError => new BaseResponse(ResponseStatus.Error) - { - Message = modelError.Value.Errors.FirstOrDefault().ErrorMessage - }).FirstOrDefault()); - } -} +using Microsoft.AspNetCore.Mvc; + +namespace OsmoDoc.API.Models; + +public enum ResponseStatus +{ + Success, + Fail, + Error +} + +public class BaseResponse +{ + public BaseResponse(ResponseStatus status) => this.Status = status; + public ResponseStatus? Status { get; set; } + public string? Base64 { get; set; } + public string? AuthToken { get; set; } + public string? Message { get; set; } + public string? StackTrace { get; set; } +} + +public class ModelValidationBadRequest +{ + public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext) + { + return new BadRequestObjectResult(actionContext.ModelState + .Where(modelError => modelError.Value.Errors.Any()) + .Select(modelError => new BaseResponse(ResponseStatus.Error) + { + Message = modelError.Value.Errors.FirstOrDefault().ErrorMessage + }).FirstOrDefault()); + } +} diff --git a/OsmoDoc.API/Models/LoginRequestDTO.cs b/OsmoDoc.API/Models/LoginRequestDTO.cs new file mode 100644 index 0000000..bf7f680 --- /dev/null +++ b/OsmoDoc.API/Models/LoginRequestDTO.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class LoginRequestDTO +{ + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email format")] + public string Email { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DocumentService.API/Models/PdfGenerationRequestDTO.cs b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs similarity index 83% rename from DocumentService.API/Models/PdfGenerationRequestDTO.cs rename to OsmoDoc.API/Models/PdfGenerationRequestDTO.cs index 8408128..f88a849 100644 --- a/DocumentService.API/Models/PdfGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs @@ -1,13 +1,13 @@ -using DocumentService.Pdf.Models; -using System.ComponentModel.DataAnnotations; - -namespace DocumentService.API.Models; - -public class PdfGenerationRequestDTO -{ - [Required(ErrorMessage = "Base64 string for PDF template is required")] - public string? Base64 { get; set; } - [Required(ErrorMessage = "Data to be modified in PDF is required")] - public DocumentData? DocumentData { get; set; } - public string? SerializedEjsDataJson { get; set; } -} +using OsmoDoc.Pdf.Models; +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class PdfGenerationRequestDTO +{ + [Required(ErrorMessage = "Base64 string for PDF template is required")] + public string? Base64 { get; set; } + [Required(ErrorMessage = "Data to be modified in PDF is required")] + public DocumentData? DocumentData { get; set; } + public string? SerializedEjsDataJson { get; set; } +} diff --git a/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs new file mode 100644 index 0000000..26d5c21 --- /dev/null +++ b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class RevokeTokenRequestDTO +{ + [Required(ErrorMessage = "Token is required")] + public string Token { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DocumentService.API/Models/WordGenerationRequestDTO.cs b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs similarity index 73% rename from DocumentService.API/Models/WordGenerationRequestDTO.cs rename to OsmoDoc.API/Models/WordGenerationRequestDTO.cs index c2462a1..41d1914 100644 --- a/DocumentService.API/Models/WordGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs @@ -1,24 +1,24 @@ -using DocumentService.Word.Models; -using System.ComponentModel.DataAnnotations; - -namespace DocumentService.API.Models; - - -public class WordGenerationRequestDTO -{ - [Required(ErrorMessage = "Base64 string for Word template is required")] - public string? Base64 { get; set; } - [Required(ErrorMessage = "Data to be modified in Word file is required")] - public WordDocumentDataRequestDTO? DocumentData { get; set; } -} - -public class WordContentDataRequestDTO : ContentData -{ - public string? ImageExtension { get; set; } -} - -public class WordDocumentDataRequestDTO -{ - public List Placeholders { get; set; } - public List TablesData { get; set; } -} +using OsmoDoc.Word.Models; +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + + +public class WordGenerationRequestDTO +{ + [Required(ErrorMessage = "Base64 string for Word template is required")] + public string? Base64 { get; set; } + [Required(ErrorMessage = "Data to be modified in Word file is required")] + public WordDocumentDataRequestDTO? DocumentData { get; set; } +} + +public class WordContentDataRequestDTO : ContentData +{ + public string? ImageExtension { get; set; } +} + +public class WordDocumentDataRequestDTO +{ + public List Placeholders { get; set; } = new List(); + public List TablesData { get; set; } = new List(); +} diff --git a/DocumentService.API/DocumentService.API.csproj b/OsmoDoc.API/OsmoDoc.API.csproj similarity index 83% rename from DocumentService.API/DocumentService.API.csproj rename to OsmoDoc.API/OsmoDoc.API.csproj index bcd5563..a36d4ff 100644 --- a/DocumentService.API/DocumentService.API.csproj +++ b/OsmoDoc.API/OsmoDoc.API.csproj @@ -8,10 +8,11 @@ + - + \ No newline at end of file diff --git a/DocumentService.API/DocumentService.API.sln b/OsmoDoc.API/OsmoDoc.API.sln similarity index 86% rename from DocumentService.API/DocumentService.API.sln rename to OsmoDoc.API/OsmoDoc.API.sln index ee22b37..94f94c8 100644 --- a/DocumentService.API/DocumentService.API.sln +++ b/OsmoDoc.API/OsmoDoc.API.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34031.279 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.API", "DocumentService.API.csproj", "{A99B82C1-0758-4FDA-8D3B-5F11E99896F1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OsmoDoc.API", "OsmoDoc.API.csproj", "{A99B82C1-0758-4FDA-8D3B-5F11E99896F1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/DocumentService.API/Program.cs b/OsmoDoc.API/Program.cs similarity index 70% rename from DocumentService.API/Program.cs rename to OsmoDoc.API/Program.cs index 728b1ac..2f58b91 100644 --- a/DocumentService.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -1,137 +1,174 @@ -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Serilog.Events; -using Serilog; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.OpenApi.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; -using Swashbuckle.AspNetCore.Filters; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Controller Services -builder.Services.AddControllers(options => options.Filters.Add(new ProducesAttribute("application/json"))) - .AddJsonOptions(options => - { - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); - -// Load .env file -string root = Directory.GetCurrentDirectory(); -string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); -DocumentService.API.DotEnv.Load(dotenv); - -// Configure request size limit -long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); - -// Configure request size for Kestrel server - ASP.NET Core project templates use Kestrel by default when not hosted with IIS -builder.Services.Configure(options => -{ - options.Limits.MaxRequestBodySize = requestBodySizeLimitBytes; -}); - -// Configure request size for IIS server -builder.Services.Configure(options => -{ - options.MaxRequestBodySize = requestBodySizeLimitBytes; -}); - -// AutoMapper Services -builder.Services.AddAutoMapper(typeof(Program)); - -// Swagger UI Services -builder.Services.AddSwaggerGen(options => -{ - options.SwaggerDoc("v1", new OpenApiInfo { Title = "DocumentService API", Version = "v1" }); - - options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Name = "Authorization", - Description = "Standard Authorization header using the Bearer Scheme (\"bearer {token}\")", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey - }); - - options.OperationFilter(); -}); - -// Authentication -builder.Services.AddAuthentication(options => -{ - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}) -.AddJwtBearer(options => -{ - string JWT_KEY = Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified"); - - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)), - ValidateIssuer = false, - ValidateAudience = false, - // Following code is to allow us to custom handle expiry - // Here check expiry as nullable - ClockSkew = TimeSpan.Zero, - ValidateLifetime = true, - LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => - { - // Clone the validation parameters, and remove the defult lifetime validator - TokenValidationParameters clonedParameters = validationParameters.Clone(); - clonedParameters.LifetimeValidator = null; - - // If token expiry time is not null, then validate lifetime with skewed clock - if (expires != null) - { - Validators.ValidateLifetime(notBefore, expires, securityToken, clonedParameters); - } - - return true; - } - }; -}); - -// Configure Error Response from Model Validations -builder.Services.AddMvc().ConfigureApiBehaviorOptions(options => -{ - options.InvalidModelStateResponseFactory = actionContext => - { - return ModelValidationBadRequest.ModelValidationErrorResponse(actionContext); - }; -}); - -// Logging service Serilogs -builder.Logging.AddSerilog(); -Log.Logger = new LoggerConfiguration() - .WriteTo.File( - path: "wwwroot/logs/log-.txt", - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}{NewLine}{NewLine}", - rollingInterval: RollingInterval.Day, - restrictedToMinimumLevel: LogEventLevel.Information - ).CreateLogger(); - -WebApplication app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseStaticFiles(); - -app.UseHttpsRedirection(); - -app.UseAuthentication(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); +using Microsoft.AspNetCore.Mvc; +using Serilog.Events; +using Serilog; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using Swashbuckle.AspNetCore.Filters; +using OsmoDoc.Pdf; +using StackExchange.Redis; +using OsmoDoc.API.Models; +using OsmoDoc.Services; +using System.IdentityModel.Tokens.Jwt; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Controller Services +builder.Services.AddControllers(options => options.Filters.Add(new ProducesAttribute("application/json"))) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + +// Load .env file +string root = Directory.GetCurrentDirectory(); +string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); +OsmoDoc.API.DotEnv.Load(dotenv); + +// Initialize PDF tool path once at startup +OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( + builder.Environment.WebRootPath, + builder.Configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value! +); + +// Register REDIS service +builder.Services.AddSingleton( + ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_URL") ?? throw new Exception("No REDIS URL specified")) +); +builder.Services.AddScoped(); + +// Configure request size limit +long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); + +// Configure request size for Kestrel server - ASP.NET Core project templates use Kestrel by default when not hosted with IIS +builder.Services.Configure(options => +{ + options.Limits.MaxRequestBodySize = requestBodySizeLimitBytes; +}); + +// Configure request size for IIS server +builder.Services.Configure(options => +{ + options.MaxRequestBodySize = requestBodySizeLimitBytes; +}); + +// AutoMapper Services +builder.Services.AddAutoMapper(typeof(Program)); + +// Swagger UI Services +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo { Title = "OsmoDoc API", Version = "v1" }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "Standard Authorization header using the Bearer Scheme (\"bearer {token}\")", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + options.OperationFilter(); +}); + +// Authentication +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + string JWT_KEY = Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified"); + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)), + ValidateIssuer = false, + ValidateAudience = false, + // Following code is to allow us to custom handle expiry + // Here check expiry as nullable + ClockSkew = TimeSpan.Zero, + ValidateLifetime = true, + LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => + { + // Clone the validation parameters, and remove the defult lifetime validator + TokenValidationParameters clonedParameters = validationParameters.Clone(); + clonedParameters.LifetimeValidator = null; + + // If token expiry time is not null, then validate lifetime with skewed clock + if (expires != null) + { + Validators.ValidateLifetime(notBefore, expires, securityToken, clonedParameters); + } + + return true; + } + }; + + options.Events = new JwtBearerEvents + { + OnTokenValidated = async context => + { + IRedisTokenStoreService tokenStore = context.HttpContext.RequestServices.GetRequiredService(); + JwtSecurityToken? token = context.SecurityToken as JwtSecurityToken; + string tokenString = string.Empty; + + if (context.Request.Headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues authHeader) && + authHeader.ToString().StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + tokenString = authHeader.ToString().Substring("Bearer ".Length).Trim(); + } + + if (!await tokenStore.IsTokenValidAsync(tokenString, context.HttpContext.RequestAborted)) + { + context.Fail("Token has been revoked."); + } + } + }; +}); + +// Configure Error Response from Model Validations +builder.Services.AddMvc().ConfigureApiBehaviorOptions(options => +{ + options.InvalidModelStateResponseFactory = actionContext => + { + return ModelValidationBadRequest.ModelValidationErrorResponse(actionContext); + }; +}); + +// Logging service Serilogs +builder.Logging.AddSerilog(); +Log.Logger = new LoggerConfiguration() + .WriteTo.File( + path: "wwwroot/logs/log-.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}{NewLine}{NewLine}", + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: LogEventLevel.Information + ).CreateLogger(); + +WebApplication app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseStaticFiles(); + +app.UseHttpsRedirection(); + +app.UseAuthentication(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/DocumentService.API/Properties/launchSettings.json b/OsmoDoc.API/Properties/launchSettings.json similarity index 100% rename from DocumentService.API/Properties/launchSettings.json rename to OsmoDoc.API/Properties/launchSettings.json diff --git a/DocumentService.API/appsettings.Development.json b/OsmoDoc.API/appsettings.Development.json similarity index 100% rename from DocumentService.API/appsettings.Development.json rename to OsmoDoc.API/appsettings.Development.json diff --git a/DocumentService.API/appsettings.json b/OsmoDoc.API/appsettings.json similarity index 100% rename from DocumentService.API/appsettings.json rename to OsmoDoc.API/appsettings.json diff --git a/DocumentService.API/wwwroot/Tools/wkhtmltopdf.exe b/OsmoDoc.API/wwwroot/Tools/wkhtmltopdf.exe similarity index 100% rename from DocumentService.API/wwwroot/Tools/wkhtmltopdf.exe rename to OsmoDoc.API/wwwroot/Tools/wkhtmltopdf.exe diff --git a/DocumentService.sln b/OsmoDoc.sln similarity index 80% rename from DocumentService.sln rename to OsmoDoc.sln index 2a3de81..d4c665f 100644 --- a/DocumentService.sln +++ b/OsmoDoc.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentService", "DocumentService\DocumentService.csproj", "{AC14A26A-220C-487E-9D7B-BB0548D86318}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OsmoDoc", "OsmoDoc\OsmoDoc.csproj", "{AC14A26A-220C-487E-9D7B-BB0548D86318}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentService.API", "DocumentService.API\DocumentService.API.csproj", "{2D2738C1-032B-441A-9298-1722A4915BD4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OsmoDoc.API", "OsmoDoc.API\OsmoDoc.API.csproj", "{2D2738C1-032B-441A-9298-1722A4915BD4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/OsmoDoc/OsmoDoc.csproj b/OsmoDoc/OsmoDoc.csproj new file mode 100644 index 0000000..f0b5443 --- /dev/null +++ b/OsmoDoc/OsmoDoc.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + True + enable + + OsmoDoc + 1.0.0 + Osmosys Software Solutions + Osmosys Software Solutions + A library for generating PDF documents from HTML and EJS templates using wkhtmltopdf. + https://github.com/OsmosysSoftware/osmodoc + pdf;html;ejs;wkhtmltopdf;document generation + MIT + https://github.com/OsmosysSoftware/osmodoc + git + + + + + + + + + + diff --git a/OsmoDoc/Pdf/Models/ContentMetaData.cs b/OsmoDoc/Pdf/Models/ContentMetaData.cs new file mode 100644 index 0000000..549f187 --- /dev/null +++ b/OsmoDoc/Pdf/Models/ContentMetaData.cs @@ -0,0 +1,9 @@ +namespace OsmoDoc.Pdf.Models; + +public class ContentMetaData +{ + public string Placeholder { get; set; } + public string Content { get; set; } +} + + diff --git a/OsmoDoc/Pdf/Models/DocumentData.cs b/OsmoDoc/Pdf/Models/DocumentData.cs new file mode 100644 index 0000000..3527fc4 --- /dev/null +++ b/OsmoDoc/Pdf/Models/DocumentData.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OsmoDoc.Pdf.Models; + +public class DocumentData +{ + public List Placeholders { get; set; } = new List(); +} diff --git a/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs b/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs new file mode 100644 index 0000000..92ae174 --- /dev/null +++ b/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs @@ -0,0 +1,7 @@ +namespace OsmoDoc.Pdf; + +public static class OsmoDocPdfConfig +{ + /// Required path to wkhtmltopdf binary, e.g. "wkhtmltopdf" on Linux + public static string? WkhtmltopdfPath { get; set; } +} diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs new file mode 100644 index 0000000..a045d16 --- /dev/null +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -0,0 +1,271 @@ +using OsmoDoc.Pdf.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace OsmoDoc.Pdf; + +public class PdfDocumentGenerator +{ + /// + /// Generates a PDF document from an HTML or EJS template. + /// + /// The path to the HTML or EJS template file. + /// A list of content metadata to replace placeholders in the template. + /// The desired output path for the generated PDF file. + /// A boolean indicating whether the template is an EJS file. + /// JSON string containing data for EJS template rendering. Required if isEjsTemplate is true. + public async static Task GeneratePdf(string templatePath, List metaDataList, string outputFilePath, bool isEjsTemplate, string? serializedEjsDataJson) + { + try + { + if (metaDataList is null) + { + throw new ArgumentNullException(nameof(metaDataList)); + } + + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } + + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } + + if (string.IsNullOrWhiteSpace(OsmoDocPdfConfig.WkhtmltopdfPath) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + throw new Exception("WkhtmltopdfPath is not set in OsmoDocPdfConfig."); + } + + if (!File.Exists(templatePath)) + { + throw new Exception("The file path you provided is not valid."); + } + + if (isEjsTemplate) + { + // Validate if template in file path is an ejs file + if (Path.GetExtension(templatePath).ToLower() != ".ejs") + { + throw new Exception("Input template should be a valid EJS file"); + } + + // Convert ejs file to an equivalent html + templatePath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + } + + // Modify html template with content data and generate pdf + string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); + await ConvertHtmlToPdf(OsmoDocPdfConfig.WkhtmltopdfPath, modifiedHtmlFilePath, outputFilePath); + + if (isEjsTemplate) + { + // If input template was an ejs file, then the template path contains path to html converted from ejs + if (File.Exists(templatePath) && Path.GetExtension(templatePath).ToLower() == ".html") + { + // If template path contains path to converted html template then delete it + File.Delete(templatePath); + } + } + } + catch (Exception) + { + throw; + } + } + + private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) + { + string htmlContent = File.ReadAllText(templatePath); + + foreach (ContentMetaData metaData in metaDataList) + { + htmlContent = htmlContent.Replace($"{{{{{metaData.Placeholder}}}}}", metaData.Content); + } + + string? directoryPath = Path.GetDirectoryName(outputFilePath); + if (directoryPath == null) + { + throw new Exception($"No directory found for the path: {outputFilePath}"); + } + string uniqueId = Guid.NewGuid().ToString("N"); + string tempHtmlFilePath = Path.Combine(directoryPath, $"Modified_{uniqueId}"); + string tempHtmlFile = Path.Combine(tempHtmlFilePath, "modifiedHtml.html"); + + if (!Directory.Exists(tempHtmlFilePath)) + { + Directory.CreateDirectory(tempHtmlFilePath); + } + + File.WriteAllText(tempHtmlFile, htmlContent); + return tempHtmlFile; + } + + private async static Task ConvertHtmlToPdf(string? wkhtmltopdfPath, string modifiedHtmlFilePath, string outputFilePath) + { + string fileName; + string arguments; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + fileName = "wkhtmltopdf"; + arguments = $"\"{modifiedHtmlFilePath}\" \"{outputFilePath}\""; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (string.IsNullOrWhiteSpace(wkhtmltopdfPath)) + { + throw new Exception("wkhtmltopdf path must be explicitly set on Windows."); + } + + string fullPath = Path.GetFullPath(wkhtmltopdfPath); + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"wkhtmltopdf binary not found at: {fullPath}"); + } + + fileName = fullPath; + arguments = $"\"{modifiedHtmlFilePath}\" \"{outputFilePath}\""; + } + else + { + throw new PlatformNotSupportedException("Unsupported operating system."); + } + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = psi; + process.Start(); + await process.WaitForExitAsync(); + string output = await process.StandardOutput.ReadToEndAsync(); + string errors = await process.StandardError.ReadToEndAsync(); + + if (process.ExitCode != 0) + { + throw new Exception($"Error during PDF generation: {errors} (Exit Code: {process.ExitCode})"); + } + } + + // Delete the temporary modified HTML file + if (File.Exists(modifiedHtmlFilePath)) + { + File.Delete(modifiedHtmlFilePath); + } + } + + private async static Task ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string? ejsDataJson) + { + // Generate directory + string? directoryPath = Path.GetDirectoryName(outputFilePath); + if (directoryPath == null) + { + throw new Exception($"No directory found for the path: {outputFilePath}"); + } + string uniqueId = Guid.NewGuid().ToString("N"); + string tempDirectoryFilePath = Path.Combine(directoryPath, $"Temp_{uniqueId}"); + + if (!Directory.Exists(tempDirectoryFilePath)) + { + Directory.CreateDirectory(tempDirectoryFilePath); + } + + // Generate file path to converted html template + string tempHtmlFilePath = Path.Combine(tempDirectoryFilePath, "htmlTemplate.html"); + + // If the ejs data json is invalid then throw exception + if (!string.IsNullOrWhiteSpace(ejsDataJson) && !IsValidJSON(ejsDataJson)) + { + throw new Exception("Received invalid JSON data for EJS template"); + } + + // Write json data string to json file + string ejsDataJsonFilePath = Path.Combine(tempDirectoryFilePath, "ejsData.json"); + string contentToWrite = ejsDataJson ?? "{}"; + File.WriteAllText(ejsDataJsonFilePath, contentToWrite); + + string commandLine = "cmd.exe"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + commandLine = "ejs"; + } + string arguments = EjsToHtmlArgumentsBasedOnOS(ejsFilePath, ejsDataJsonFilePath, tempHtmlFilePath); + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = commandLine, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = psi; + process.Start(); + await process.WaitForExitAsync(); + string output = await process.StandardOutput.ReadToEndAsync(); + string errors = await process.StandardError.ReadToEndAsync(); + + if (process.ExitCode != 0) + { + throw new Exception($"Error during EJS to HTML conversion: {errors} (Exit Code: {process.ExitCode})"); + } + } + + // Delete json data file + if (File.Exists(ejsDataJsonFilePath)) + { + File.Delete(ejsDataJsonFilePath); + } + + return tempHtmlFilePath; + } + + private static bool IsValidJSON(string json) + { + try + { + JToken.Parse(json); + return true; + } + catch (JsonReaderException) + { + return false; + } + } + + private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejsDataJsonFilePath, string tempHtmlFilePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"/C ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return $"\"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; + } + else + { + throw new Exception("Unknown operating system"); + } + } +} diff --git a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs new file mode 100644 index 0000000..ddbfde5 --- /dev/null +++ b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace OsmoDoc.Services; + +public interface IRedisTokenStoreService +{ + Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default); + Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default); + Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/OsmoDoc/Services/RedisTokenStoreService.cs b/OsmoDoc/Services/RedisTokenStoreService.cs new file mode 100644 index 0000000..f54a688 --- /dev/null +++ b/OsmoDoc/Services/RedisTokenStoreService.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace OsmoDoc.Services; + +public class RedisTokenStoreService : IRedisTokenStoreService +{ + private readonly IDatabase _db; + private const string KeyPrefix = "valid_token:"; + + public RedisTokenStoreService(IConnectionMultiplexer redis) + { + this._db = redis.GetDatabase(); + } + + public Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default) + { + // Check if operation was cancelled before starting + cancellationToken.ThrowIfCancellationRequested(); + + return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new + { + issuedTo = email, + issuedAt = DateTime.UtcNow + })); + } + + public Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return this._db.KeyExistsAsync($"{KeyPrefix}{token}"); + } + + public Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return this._db.KeyDeleteAsync($"{KeyPrefix}{token}"); + } +} \ No newline at end of file diff --git a/OsmoDoc/Word/Models/ContentData.cs b/OsmoDoc/Word/Models/ContentData.cs new file mode 100644 index 0000000..512eb79 --- /dev/null +++ b/OsmoDoc/Word/Models/ContentData.cs @@ -0,0 +1,29 @@ +namespace OsmoDoc.Word.Models; + + +/// +/// Represents the data for a content placeholder in a Word document. +/// +public class ContentData +{ + /// + /// Gets or sets the placeholder name. + /// + public string Placeholder { get; set; } + + /// + /// Gets or sets the content to replace the placeholder with. + /// + public string Content { get; set; } + + /// + /// Gets or sets the content type of the placeholder (text or image). + /// + public ContentType ContentType { get; set; } + + /// + /// Gets or sets the parent body of the placeholder (none or table). + /// + + public ParentBody ParentBody { get; set; } +} diff --git a/OsmoDoc/Word/Models/DocumentData.cs b/OsmoDoc/Word/Models/DocumentData.cs new file mode 100644 index 0000000..ea0e978 --- /dev/null +++ b/OsmoDoc/Word/Models/DocumentData.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OsmoDoc.Word.Models; + + +/// +/// Represents the data for a Word document, including content placeholders and table data. +/// +public class DocumentData +{ + /// + /// Gets or sets the list of content placeholders in the document. + /// + public List Placeholders { get; set; } + + /// + /// Gets or sets the list of table data in the document. + /// + + public List TablesData { get; set; } +} diff --git a/OsmoDoc/Word/Models/Enums.cs b/OsmoDoc/Word/Models/Enums.cs new file mode 100644 index 0000000..d957dad --- /dev/null +++ b/OsmoDoc/Word/Models/Enums.cs @@ -0,0 +1,35 @@ +namespace OsmoDoc.Word.Models; + + +/// +/// Represents the content type of a placeholder in a Word document. +/// +public enum ContentType +{ + /// + /// The placeholder represents text content. + /// + Text = 0, + + /// + /// The placeholder represents an image. + /// + Image = 1 +} + +/// +/// Represents the parent body of a placeholder in a Word document. +/// +public enum ParentBody +{ + /// + /// The placeholder does not have a parent body. + /// + None = 0, + + /// + /// The placeholder belongs to a table. + /// + + Table = 1 +} diff --git a/OsmoDoc/Word/Models/TableData.cs b/OsmoDoc/Word/Models/TableData.cs new file mode 100644 index 0000000..6e47f98 --- /dev/null +++ b/OsmoDoc/Word/Models/TableData.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace OsmoDoc.Word.Models; + + +/// +/// Represents the data for a table in a Word document. +/// +public class TableData +{ + /// + /// Gets or sets the position of the table in the document. + /// + public int TablePos { get; set; } + + /// + /// Gets or sets the list of dictionaries representing the data for each row in the table. + /// Each dictionary contains column header-value pairs. + /// + + public List> Data { get; set; } +} diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs new file mode 100644 index 0000000..49c1588 --- /dev/null +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -0,0 +1,336 @@ +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Drawing.Wordprocessing; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using OsmoDoc.Word.Models; +using NPOI.XWPF.UserModel; +using System; +using System.Collections.Generic; +using System.IO; +using IOPath = System.IO.Path; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Net.Http; + +namespace OsmoDoc.Word; + +/// +/// Provides functionality to generate Word documents based on templates and data. +/// +public static class WordDocumentGenerator +{ + /// + /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. + /// + /// The file path of the template document. + /// The data to replace the placeholders in the template. + /// The file path to save the generated document. + public async static Task GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) + { + try + { + if (string.IsNullOrWhiteSpace(templateFilePath)) + { + throw new ArgumentNullException(nameof(templateFilePath)); + } + + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } + + List contentData = documentData.Placeholders; + List tablesData = documentData.TablesData; + + // Creating dictionaries for each type of placeholders + Dictionary textPlaceholders = new Dictionary(); + Dictionary tableContentPlaceholders = new Dictionary(); + Dictionary imagePlaceholders = new Dictionary(); + + foreach (ContentData content in contentData) + { + if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) + { + string placeholder = "{" + content.Placeholder + "}"; + textPlaceholders.TryAdd(placeholder, content.Content); + } + else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) + { + string placeholder = content.Placeholder; + imagePlaceholders.TryAdd(placeholder, content.Content); + } + else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + { + string placeholder = "{" + content.Placeholder + "}"; + tableContentPlaceholders.TryAdd(placeholder, content.Content); + } + } + + // Create document of the template + XWPFDocument document = await GetXWPFDocument(templateFilePath); + + // For each element in the document + foreach (IBodyElement element in document.BodyElements) + { + if (element.ElementType == BodyElementType.PARAGRAPH) + { + // If element is a paragraph + XWPFParagraph paragraph = (XWPFParagraph)element; + + // If the paragraph is empty string or the placeholder regex does not match then continue + if (paragraph.ParagraphText == string.Empty || !new Regex(@"{[a-zA-Z]+}").IsMatch(paragraph.ParagraphText)) + { + continue; + } + + // Replace placeholders in paragraph with values + paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); + } + else if (element.ElementType == BodyElementType.TABLE) + { + // If element is a table + XWPFTable table = (XWPFTable)element; + + // Replace placeholders in a table + table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); + + // Populate the table with data if it is passed in tablesData list + foreach (TableData insertData in tablesData) + { + if (insertData.TablePos >= 1 && insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) + { + table = PopulateTable(table, insertData); + } + } + } + } + + // Write the document to output file path and close the document + WriteDocument(document, outputFilePath); + document.Close(); + + /* + * Image Replacement is done after writing the document here, + * because for Text Replacement, NPOI package is being used + * and for Image Replacement, OpeXML package is used. + * Since both the packages have different execution method, so they are handled separately + */ + // Replace all the image placeholders in the output file + await ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); + } + catch (Exception) + { + throw; + } + } + + /// + /// Retrieves an instance of XWPFDocument from the specified document file path. + /// + /// The file path of the Word document. + /// An instance of XWPFDocument representing the Word document. + private async static Task GetXWPFDocument(string docFilePath) + { + byte[] fileBytes = await File.ReadAllBytesAsync(docFilePath); + using MemoryStream memoryStream = new MemoryStream(fileBytes); + return new XWPFDocument(memoryStream); + } + + /// + /// Writes the XWPFDocument to the specified file path. + /// + /// The XWPFDocument to write. + /// The file path to save the document. + private static void WriteDocument(XWPFDocument document, string filePath) + { + string? directory = IOPath.GetDirectoryName(filePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + using (FileStream writeStream = File.Create(filePath)) + { + document.Write(writeStream); + } + } + + /// + /// Replaces the text placeholders in a paragraph with the specified values. + /// + /// The XWPFParagraph containing the placeholders. + /// The dictionary of text placeholders and their corresponding values. + /// The updated XWPFParagraph. + private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) + { + // Get a list of all placeholders in the current paragraph + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + .Cast() + .Select(s => s.Groups[0].Value).ToList(); + + // For each placeholder in paragraph + foreach (string placeholder in placeholdersTobeReplaced) + { + // Replace text placeholders in paragraph with values + if (textPlaceholders.ContainsKey(placeholder)) + { + paragraph.ReplaceText(placeholder, textPlaceholders[placeholder]); + } + + paragraph.SpacingAfter = 0; + } + + return paragraph; + } + + /// + /// Replaces the text placeholders in a table with the specified values. + /// + /// The XWPFTable containing the placeholders. + /// The dictionary of table content placeholders and their corresponding values. + /// The updated XWPFTable. + private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary tableContentPlaceholders) + { + // Loop through each cell of the table + foreach (XWPFTableRow row in table.Rows) + { + foreach (XWPFTableCell cell in row.GetTableCells()) + { + foreach (XWPFParagraph paragraph in cell.Paragraphs) + { + // Get a list of all placeholders in the current cell + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + .Cast() + .Select(s => s.Groups[0].Value).ToList(); + + // For each placeholder in the cell + foreach (string placeholder in placeholdersTobeReplaced) + { + // replace the placeholder with its value + if (tableContentPlaceholders.ContainsKey(placeholder)) + { + paragraph.ReplaceText(placeholder, tableContentPlaceholders[placeholder]); + } + } + } + } + } + + return table; + } + + /// + /// Populates a table with the specified data. + /// + /// The XWPFTable to populate. + /// The data to populate the table. + /// The updated XWPFTable. + private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) + { + // Get the header row + XWPFTableRow headerRow = table.GetRow(0); + + // Return if no header row found or if it does not have any column + if (headerRow == null || headerRow.GetTableCells() == null || headerRow.GetTableCells().Count <= 0) + { + return table; + } + + // For each row's data stored in table data + foreach (Dictionary rowData in tableData.Data) + { + XWPFTableRow row = table.CreateRow(); // This is a DATA row, not header + + int columnCount = headerRow.GetTableCells().Count; // Read from header + for (int cellNumber = 0; cellNumber < columnCount; cellNumber++) + { + // Ensure THIS data row has enough cells + while (row.GetTableCells().Count <= cellNumber) + { + row.AddNewTableCell(); + } + + // Now populate the cell in this data row + XWPFTableCell cell = row.GetCell(cellNumber); + string columnHeader = headerRow.GetCell(cellNumber).GetText(); + if (rowData.ContainsKey(columnHeader)) + { + cell.SetText(rowData[columnHeader]); + } + } + } + + return table; + } + + /// + /// Replaces the image placeholders in the output file with the specified images. + /// + /// The input file path containing the image placeholders. + /// The output file path where the updated document will be saved. + /// The dictionary of image placeholders and their corresponding image paths. + private async static Task ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) + { + byte[] docBytes = await File.ReadAllBytesAsync(inputFilePath); + + // Write document bytes to memory asynchronously + using (MemoryStream memoryStream = new MemoryStream()) + { + await memoryStream.WriteAsync(docBytes, 0, docBytes.Length); + memoryStream.Position = 0; + + using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) + { + MainDocumentPart? mainDocumentPart = wordDocument.MainDocumentPart; + + // Get a list of drawings (images) + IEnumerable drawings = new List(); + if (mainDocumentPart != null) + { + drawings = mainDocumentPart.Document.Descendants().ToList(); + } + + /* + * FIXME: Look on how we can improve this loop operation. + */ + foreach (Drawing drawing in drawings) + { + DocProperties? docProperty = drawing.Descendants().FirstOrDefault(); + + // If drawing / image name is present in imagePlaceholders dictionary, then replace image + if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) + { + List drawingBlips = drawing.Descendants().ToList(); + + foreach (Blip blip in drawingBlips) + { + OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); + + string imagePath = imagePlaceholders[docProperty.Name]; + + // Asynchronously download image data using HttpClient + using HttpClient httpClient = new HttpClient(); + byte[] imageData = await httpClient.GetByteArrayAsync(imagePath); + + using (Stream partStream = imagePart.GetStream(FileMode.OpenOrCreate, FileAccess.Write)) + { + // Asynchronously write image data to the part stream + await partStream.WriteAsync(imageData, 0, imageData.Length); + partStream.SetLength(imageData.Length); // Ensure the stream is truncated if new data is smaller + } + } + } + } + } + // Overwrite the output file asynchronously + using (FileStream fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + // Reset MemoryStream position before writing to fileStream + memoryStream.Position = 0; + await memoryStream.CopyToAsync(fileStream); + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index fcbf352..1ada5b7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# DocumentService -DocumentService is a library with the following functions +# OsmoDoc +OsmoDoc is a library with the following functions 1. **Generate Word documents** - Read Word document files as a template and replace the placeholder with actual data. 2. **Generate PDF documents** - Read an HTML file as a template and replace placeholders with actual data. Convert the HTML file to PDF @@ -24,7 +24,7 @@ Setting up the app in a Docker-based environment enables developers of non-Windo 1. [Install Docker](https://docs.docker.com/engine/install/) on your machine. Choose to follow the instructions based on your device OS. 2. [Install Docker Compose](https://docs.docker.com/compose/install/). A separate installation is required for Linux-based OS. If you are using Windows or macOS, installing the Docker Desktop app includes Docker Compose. -3. Clone the project `document-service`. +3. Clone the project `osmodoc`. 4. (Optional) [Install Docker Extension for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker). 5. In the root directory of the project, create a new file `.env`. 6. Copy data from [example template](.env.example) into `.env`. Then set suitable JWT key. @@ -49,7 +49,7 @@ Setting up the app in a Docker-based environment enables developers of non-Windo ``` 8. Ensure Docker is running. -9. Execute the following commands to dockerize `document-service` using `docker-compose.yaml`: +9. Execute the following commands to dockerize `osmodoc` using `docker-compose.yaml`: ```shell # build the container @@ -100,7 +100,7 @@ docker compose up - Finish Installation. ## Including wkhtmltopdf executable file to build package -- Go to the location to the bin files of your project where the DocumentService DLL is located. +- Go to the location to the bin files of your project where the OsmoDoc DLL is located. - Create a folder called Tools and place the wkhtmltopdf.exe file there. wkhtmltopdf.exe can be found in the Program Files in C directory after it is installed. Note: We use a Temp folder to temporarily hold the modified HTML file before converting it to a PDF file. After the conversion is done, the temporary file is removed. The code is already provided with the location of the temp file, so no modification is required in the code, and the temp folder will be used automatically. @@ -123,8 +123,8 @@ PdfDocumentGenerator.GeneratePdfByTemplate("Tools\\index.html", contentList, "To ## Word document generation ```csharp -string templateFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\Document Service Component\Testing\Document.docx"; -string outputFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\Document Service Component\Testing\Test_Output.docx"; +string templateFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\OsmoDoc Component\Testing\Document.docx"; +string outputFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\OsmoDoc Service Component\Testing\Test_Output.docx"; List tablesData = new List() { @@ -187,11 +187,11 @@ List tablesData = new List() - [wkhtmltopdf](https://wkhtmltopdf.org/) # License -The DocumentService is licensed under the [MIT](https://github.com/OsmosysSoftware/document-service/blob/main/LICENSE) license. +The OsmoDoc is licensed under the [MIT](https://github.com/OsmosysSoftware/osmodoc/blob/main/LICENSE) license. ## 👏 Big Thanks to Our Contributors - - Contributors + + Contributors We appreciate the time and effort put in by all contributors to make this project better! \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 8df18d6..6eb71ca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,18 @@ services: - document-service: + redis: + image: redis:7 + container_name: redis + env_file: + - .env + ports: + - ${REDIS_PORT}:6379 + + osmodoc: build: context: . dockerfile: Dockerfile - image: document-service-docker - container_name: document-service-api + image: osmodoc-docker + container_name: osmodoc-api env_file: - .env ports: diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html similarity index 81% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html index 240969e..c7f4db0 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html @@ -67,11 +67,11 @@
-
+
-

Class ContentData +

Class ContentData

Represents the data for a content placeholder in a Word document.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class ContentData

Properties

- -

Content

+ +

Content

Gets or sets the content to replace the placeholder with.
Declaration
@@ -135,8 +135,8 @@
Property Value
- -

ContentType

+ +

ContentType

Gets or sets the content type of the placeholder (text or image).
Declaration
@@ -153,13 +153,13 @@
Property Value
- ContentType + ContentType - -

ParentBody

+ +

ParentBody

Gets or sets the parent body of the placeholder (none or table).
Declaration
@@ -176,13 +176,13 @@
Property Value
- ParentBody + ParentBody - -

Placeholder

+ +

Placeholder

Gets or sets the placeholder name.
Declaration
diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html similarity index 86% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html index 976b450..53b1147 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html @@ -67,18 +67,18 @@
-
+
-

Enum ContentType +

Enum ContentType

Represents the content type of a placeholder in a Word document.
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public enum ContentType
@@ -93,11 +93,11 @@

Fields - Image + Image The placeholder represents an image. - Text + Text The placeholder represents text content. diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html similarity index 84% rename from docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html index 22bee61..e7bde9c 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html @@ -67,11 +67,11 @@

-
+
-

Class DocumentData +

Class DocumentData

Represents the data for a Word document, including content placeholders and table data.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class DocumentData

Properties

- -

Placeholders

+ +

Placeholders

Gets or sets the list of content placeholders in the document.
Declaration
@@ -130,13 +130,13 @@
Property Value
- List<ContentData> + List<ContentData> - -

TablesData

+ +

TablesData

Gets or sets the list of table data in the document.
Declaration
@@ -153,7 +153,7 @@
Property Value
- List<TableData> + List<TableData> diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html similarity index 86% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html index 30027df..89ec062 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html @@ -67,18 +67,18 @@
-
+
-

Enum ParentBody +

Enum ParentBody

Represents the parent body of a placeholder in a Word document.
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public enum ParentBody
@@ -93,11 +93,11 @@

Fields - None + None The placeholder does not have a parent body. - Table + Table The placeholder belongs to a table. diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html similarity index 87% rename from docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html index 2f6f1bf..8a48600 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html @@ -67,11 +67,11 @@

-
+
-

Class TableData +

Class TableData

Represents the data for a table in a Word document.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class TableData

Properties

- -

Data

+ +

Data

Gets or sets the list of dictionaries representing the data for each row in the table. Each dictionary contains column header-value pairs.
@@ -136,8 +136,8 @@
Property Value
- -

TablePos

+ +

TablePos

Gets or sets the position of the table in the document.
Declaration
diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.html similarity index 84% rename from docs/site/10.0.2/api/DocumentService.Word.Models.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.html index bdb06e8..7d574b8 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.html @@ -5,10 +5,10 @@ - Namespace DocumentService.Word.Models + <title>Namespace OsmoDoc.Word.Models | Some Documentation - @@ -67,9 +67,9 @@
-
+
-

Namespace DocumentService.Word.Models +

Namespace OsmoDoc.Word.Models

@@ -77,18 +77,18 @@

Classes

-

ContentData

+

ContentData

Represents the data for a content placeholder in a Word document.
-

DocumentData

+

DocumentData

Represents the data for a Word document, including content placeholders and table data.
-

TableData

+

TableData

Represents the data for a table in a Word document.

Enums

-

ContentType

+

ContentType

Represents the content type of a placeholder in a Word document.
-

ParentBody

+

ParentBody

Represents the parent body of a placeholder in a Word document.
diff --git a/docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html b/docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html similarity index 85% rename from docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html rename to docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html index 68e403b..3bf7445 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html @@ -67,11 +67,11 @@
-
+
-

Class WordDocumentGenerator +

Class WordDocumentGenerator

Provides functionality to generate Word documents based on templates and data.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word
+
Assembly: OsmoDoc.dll
+
Syntax
public static class WordDocumentGenerator

Methods

- -

GenerateDocumentByTemplate(string, DocumentData, string)

+ +

GenerateDocumentByTemplate(string, DocumentData, string)

Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path.
Declaration
@@ -136,7 +136,7 @@
Parameters
The file path of the template document. - DocumentData + DocumentData documentData The data to replace the placeholders in the template. diff --git a/docs/site/10.0.2/api/DocumentService.Word.html b/docs/site/10.0.2/api/OsmoDocWord.html similarity index 91% rename from docs/site/10.0.2/api/DocumentService.Word.html rename to docs/site/10.0.2/api/OsmoDocWord.html index b4b7697..2e1c7cb 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.html +++ b/docs/site/10.0.2/api/OsmoDocWord.html @@ -5,10 +5,10 @@ - Namespace DocumentService.Word + <title>Namespace OsmoDoc.Word | Some Documentation - @@ -67,9 +67,9 @@
-
+
-

Namespace DocumentService.Word +

Namespace OsmoDoc.Word

@@ -77,7 +77,7 @@

Classes

-

WordDocumentGenerator

+

WordDocumentGenerator

Provides functionality to generate Word documents based on templates and data.
diff --git a/docs/site/10.0.2/api/toc.html b/docs/site/10.0.2/api/toc.html index 005aae4..74aad52 100644 --- a/docs/site/10.0.2/api/toc.html +++ b/docs/site/10.0.2/api/toc.html @@ -14,33 +14,33 @@