From 8b75f41168a11c8f7f50d6660f83fc8dbf0e8f68 Mon Sep 17 00:00:00 2001 From: nirikash <141118539+nirikash@users.noreply.github.com> Date: Fri, 14 Jun 2024 18:06:03 +0200 Subject: [PATCH] feat: Added links internalization (#10) * feat: Added links internalization Refs: #DEV-11660 --- README.md | 10 +++ .../converter/HtmlToPdfConverter.java | 10 ++- .../pdf/exporter/converter/PdfConverter.java | 5 +- .../PdfExporterExtensionConfiguration.java | 6 ++ .../pdf/exporter/util/HtmlProcessor.java | 11 ++- .../util/html/ExternalCssInternalizer.java | 37 +++++++++ .../exporter/util/html/HtmlLinksHelper.java | 72 +++++++++++++++++ .../exporter/util/html/LinkInternalizer.java | 8 ++ .../html/help/configuration.html | 18 +++++ .../converter/HtmlToPdfConverterTest.java | 6 +- .../exporter/converter/PdfConverterTest.java | 1 + .../pdf/exporter/util/HtmlProcessorTest.java | 8 +- .../html/ExternalCssInternalizerTest.java | 53 +++++++++++++ .../util/html/HtmlLinksHelperTest.java | 78 +++++++++++++++++++ .../PdfConverterWeasyPrintTest.java | 6 +- .../pdf/exporter/weasyprint/PdfWidthTest.java | 2 +- 16 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/ExternalCssInternalizer.java create mode 100644 src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelper.java create mode 100644 src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/LinkInternalizer.java create mode 100644 src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/ExternalCssInternalizerTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelperTest.java diff --git a/README.md b/README.md index e6e0881..2691c38 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,16 @@ If HTML logging is switched on, then in standard polarion log file there will be Here you can find out in which files HTML was stored. +### Enabling Internalization of CSS Links +The converting HTML can contain some external CSS links referencing Polarion Server, like: +```html + +``` +In case the Polarion Server is not reachable from the Weasyprint service, such links cannot be successfully resolved during the Weasyprint PDF transformation. The solution is to replace external CSS elements with internal CSS "); + + return Optional.of(inlinedContent.toString()); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelper.java b/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelper.java new file mode 100644 index 0000000..f82ab9e --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelper.java @@ -0,0 +1,72 @@ +package ch.sbb.polarion.extension.pdf.exporter.util.html; + +import ch.sbb.polarion.extension.pdf.exporter.properties.PdfExporterExtensionConfiguration; +import ch.sbb.polarion.extension.pdf.exporter.util.FileResourceProvider; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HtmlLinksHelper { + private static final Pattern LINK_PATTERN = Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); + private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("([\\w-]+)\\s*=\\s*(['\"])(.*?)\\2"); + + private final Set linkInliners; + + public HtmlLinksHelper(FileResourceProvider fileResourceProvider) { + this (Set.of( + new ExternalCssInternalizer(fileResourceProvider) + )); + } + + public HtmlLinksHelper(Set linkInliners) { + this.linkInliners = linkInliners; + } + + public String internalizeLinks(String htmlContent) { + boolean linksInternalizationEnabled = PdfExporterExtensionConfiguration.getInstance().getInternalizeExternalCss(); + if (!linksInternalizationEnabled) { + return htmlContent; + } + + Matcher matcher = LINK_PATTERN.matcher(htmlContent); + StringBuilder newHtmlContent = new StringBuilder(); + + while (matcher.find()) { + String linkTag = matcher.group(0); + + Map attributesMap = parseLinkTagAttributes(linkTag); + String replacement = inlineLinkTag(linkTag, attributesMap); + + matcher.appendReplacement(newHtmlContent, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(newHtmlContent); + + return newHtmlContent.toString(); + } + + public static Map parseLinkTagAttributes(String linkTag) { + Map attributes = new LinkedHashMap<>(); + + Matcher matcher = ATTRIBUTE_PATTERN.matcher(linkTag); + + while (matcher.find()) { + String attributeName = matcher.group(1); + String attributeValue = matcher.group(3); + attributes.put(attributeName, attributeValue); + } + return attributes; + } + + private String inlineLinkTag(String linkTag, Map attributesMap) { + return linkInliners.stream() + .map(i -> i.inline(attributesMap)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElse(linkTag); + } +} \ No newline at end of file diff --git a/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/LinkInternalizer.java b/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/LinkInternalizer.java new file mode 100644 index 0000000..5bcb560 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/pdf/exporter/util/html/LinkInternalizer.java @@ -0,0 +1,8 @@ +package ch.sbb.polarion.extension.pdf.exporter.util.html; + +import java.util.Map; +import java.util.Optional; + +public interface LinkInternalizer { + Optional inline(Map attributes); +} diff --git a/src/main/resources/webapp/pdf-exporter-admin/html/help/configuration.html b/src/main/resources/webapp/pdf-exporter-admin/html/help/configuration.html index f2dd6d7..c59a9c5 100644 --- a/src/main/resources/webapp/pdf-exporter-admin/html/help/configuration.html +++ b/src/main/resources/webapp/pdf-exporter-admin/html/help/configuration.html @@ -49,6 +49,24 @@

WeasyPrint as Service in Docker

+

Enabling Internalization of CSS Links

+ +

The converting HTML can contain some external CSS links referencing Polarion Server, like:

+
+    
+<link rel="stylesheet" href="/polarion/diff-tool-app/ui/app/_next/static/css/3c374f9daffd361a.css" data-precedence="next">
+    
+
+

In case the Polarion Server is not reachable from the Weasyprint service, such links cannot be successfully resolved during the Weasyprint PDF transformation. + The solution is to replace external CSS <link> elements with internal CSS <style> tags containing the CSS content embedded into the HTML document. + By default, CSS link internalization is disabled. To enable internalization of CSS links, it is necessary to activate the following property in file <POLARION_HOME>/etc/polarion.properties:

+
+    
+ch.sbb.polarion.extension.pdf-exporter.internalizeExternalCss=true
+    
+
+ +

PDF exporter extension to appear on a Document's properties pane

    diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/HtmlToPdfConverterTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/HtmlToPdfConverterTest.java index 07f5373..ea4338d 100644 --- a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/HtmlToPdfConverterTest.java +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/HtmlToPdfConverterTest.java @@ -15,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,6 +45,7 @@ void shouldInjectHeadAndStyle() { when(pdfTemplateProcessor.buildSizeCss(Orientation.PORTRAIT, PaperSize.A4)).thenReturn("@page {size: test;}"); when(htmlProcessor.replaceImagesAsBase64Encoded(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); String resultHtml = htmlToPdfConverter.preprocessHtml(html, Orientation.PORTRAIT, PaperSize.A4); assertThat(resultHtml).isEqualTo(""" @@ -75,6 +75,7 @@ void shouldExtendExistingHeadAndStyle() { when(pdfTemplateProcessor.buildSizeCss(Orientation.LANDSCAPE, PaperSize.A3)).thenReturn(" @page {size: test;}"); when(htmlProcessor.replaceImagesAsBase64Encoded(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); String resultHtml = htmlToPdfConverter.preprocessHtml(html, Orientation.LANDSCAPE, PaperSize.A3); assertThat(resultHtml).isEqualTo(""" @@ -107,6 +108,7 @@ void shouldExtendHeadAndAddStyle() { when(pdfTemplateProcessor.buildSizeCss(Orientation.LANDSCAPE, PaperSize.A3)).thenReturn(" @page {size: test;}"); when(htmlProcessor.replaceImagesAsBase64Encoded(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); String resultHtml = htmlToPdfConverter.preprocessHtml(html, Orientation.LANDSCAPE, PaperSize.A3); assertThat(resultHtml).isEqualTo(""" @@ -142,6 +144,7 @@ void shouldAcceptAttributesAndEmptyTags() { when(pdfTemplateProcessor.buildSizeCss(Orientation.PORTRAIT, PaperSize.A4)).thenReturn("@page {size: test;}"); when(htmlProcessor.replaceImagesAsBase64Encoded(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); String resultHtml = htmlToPdfConverter.preprocessHtml(html, Orientation.PORTRAIT, PaperSize.A4); assertThat(resultHtml).isEqualTo(""" @@ -162,6 +165,7 @@ void shouldInvokeWeasyPrint() { when(pdfTemplateProcessor.buildSizeCss(Orientation.LANDSCAPE, PaperSize.A3)).thenReturn("@page {size: test;}"); when(htmlProcessor.replaceImagesAsBase64Encoded(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); when(weasyPrintConverter.convertToPdf(resultHtml, new WeasyPrintOptions(true))).thenReturn("test content".getBytes()); byte[] result = htmlToPdfConverter.convert(origHtml, Orientation.LANDSCAPE, PaperSize.A3); diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/PdfConverterTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/PdfConverterTest.java index 6602608..953433a 100644 --- a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/PdfConverterTest.java +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/converter/PdfConverterTest.java @@ -90,6 +90,7 @@ void shouldConvertToPdfInSimplestWorkflow() { when(velocityEvaluator.evaluateVelocityExpressions(eq(documentData), anyString())).thenAnswer(a -> a.getArguments()[1]); when(pdfTemplateProcessor.processUsing(eq(exportParams), eq("testDocument"), eq("css content"), anyString())).thenReturn("test html content"); when(weasyPrintConverter.convertToPdf("test html content", new WeasyPrintOptions())).thenReturn("test document content".getBytes()); + when(htmlProcessor.internalizeLinks(anyString())).thenAnswer(a -> a.getArgument(0)); // Act byte[] result = pdfConverter.convertToPdf(exportParams, null); diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/HtmlProcessorTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/HtmlProcessorTest.java index 84b03c7..ab7c6e0 100644 --- a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/HtmlProcessorTest.java +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/HtmlProcessorTest.java @@ -9,6 +9,7 @@ import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.localization.Language; import ch.sbb.polarion.extension.pdf.exporter.rest.model.settings.localization.LocalizationModel; import ch.sbb.polarion.extension.pdf.exporter.settings.LocalizationSettings; +import ch.sbb.polarion.extension.pdf.exporter.util.html.HtmlLinksHelper; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,7 +26,8 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -37,12 +39,14 @@ class HtmlProcessorTest { private FileResourceProvider fileResourceProvider; @Mock private LocalizationSettings localizationSettings; + @Mock + private HtmlLinksHelper htmlLinksHelper; private HtmlProcessor processor; @BeforeEach void init() { - processor = new HtmlProcessor(fileResourceProvider, localizationSettings); + processor = new HtmlProcessor(fileResourceProvider, localizationSettings, htmlLinksHelper); Map deTranslations = Map.of( "draft", "Entwurf", "not reviewed", "Nicht überprüft" diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/ExternalCssInternalizerTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/ExternalCssInternalizerTest.java new file mode 100644 index 0000000..81764cf --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/ExternalCssInternalizerTest.java @@ -0,0 +1,53 @@ +package ch.sbb.polarion.extension.pdf.exporter.util.html; + +import ch.sbb.polarion.extension.pdf.exporter.util.FileResourceProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExternalCssInternalizerTest { + @Mock + private FileResourceProvider fileResourceProvider; + + @InjectMocks + private ExternalCssInternalizer cssLinkInliner; + + + @Test + void shouldReturnEmptyResultForUnknownTags() { + Optional result = cssLinkInliner.inline(Map.of("rel", "unknown")); + + assertThat(result).isEmpty(); + } + + @Test + void shouldConvertStylesheetLink() { + when(fileResourceProvider.getResourceAsBytes("my-href-location")).thenReturn("test-stylesheet".getBytes()); + Optional result = cssLinkInliner.inline(Map.of("rel", "stylesheet", "href", "my-href-location")); + + assertThat(result).isNotEmpty(); + assertThat(result.get()).isEqualTo(""); + } + + @Test + void shouldConvertStylesheetLinkAndTransferDataPrecedence() { + when(fileResourceProvider.getResourceAsBytes("my-href-location")).thenReturn("test-stylesheet".getBytes()); + Optional result = cssLinkInliner.inline(Map.of( + "rel", "stylesheet", + "href", "my-href-location", + "data-precedence", "test-data-precedence")); + + assertThat(result).isNotEmpty(); + assertThat(result.get()).isEqualTo(""" + """); + } +} \ No newline at end of file diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelperTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelperTest.java new file mode 100644 index 0000000..d15a860 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/util/html/HtmlLinksHelperTest.java @@ -0,0 +1,78 @@ +package ch.sbb.polarion.extension.pdf.exporter.util.html; + +import ch.sbb.polarion.extension.pdf.exporter.properties.PdfExporterExtensionConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class HtmlLinksHelperTest { + @Mock + private LinkInternalizer linkInternalizer1; + @Mock + private LinkInternalizer linkInternalizer2; + + @Mock + private PdfExporterExtensionConfiguration pdfExporterExtensionConfiguration; + private HtmlLinksHelper htmlLinksHelper; + private MockedStatic extensionConfigurationMockedStatic; + + @BeforeEach + void setup() { + extensionConfigurationMockedStatic = mockStatic(PdfExporterExtensionConfiguration.class); + extensionConfigurationMockedStatic.when(PdfExporterExtensionConfiguration::getInstance).thenReturn(pdfExporterExtensionConfiguration); + when(pdfExporterExtensionConfiguration.getInternalizeExternalCss()).thenReturn(true); + htmlLinksHelper = new HtmlLinksHelper(Set.of(linkInternalizer1, linkInternalizer2)); + } + + @AfterEach + void shutdown() { + extensionConfigurationMockedStatic.close(); + } + + @Test + void shouldCallInlinersForLinkTags() { + htmlLinksHelper.internalizeLinks(""" + some content"""); + + Stream.of(linkInternalizer1, linkInternalizer2) + .forEach(inliner -> verify(inliner, times(2)).inline(Map.of())); + } + + @Test + void shouldParseAttributesAndReplaceLinkTags() { + when(linkInternalizer1.inline(anyMap())).thenReturn(Optional.of("")); + String resultHtml = htmlLinksHelper.internalizeLinks(""" + some content"""); + + assertThat(resultHtml).isEqualTo(""" + some content"""); + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(linkInternalizer1).inline(captor.capture()); + assertThat(captor.getValue()).containsExactly(Map.entry("attr1", "value1"), Map.entry("attr2", "value2")); + } + + @Test + void shouldReturnUnchangedHtmlWhenPropertyNotSet() { + when(pdfExporterExtensionConfiguration.getInternalizeExternalCss()).thenReturn(false); + String resultHtml = htmlLinksHelper.internalizeLinks(""" + some content"""); + assertThat(resultHtml).isEqualTo(""" + some content"""); + verify(linkInternalizer1, times(0)).inline(anyMap()); + } +} \ No newline at end of file diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfConverterWeasyPrintTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfConverterWeasyPrintTest.java index 8c00b48..11ee93c 100644 --- a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfConverterWeasyPrintTest.java +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfConverterWeasyPrintTest.java @@ -19,6 +19,7 @@ import ch.sbb.polarion.extension.pdf.exporter.util.LiveDocHelper; import ch.sbb.polarion.extension.pdf.exporter.util.MediaUtils; import ch.sbb.polarion.extension.pdf.exporter.util.PdfTemplateProcessor; +import ch.sbb.polarion.extension.pdf.exporter.util.html.HtmlLinksHelper; import ch.sbb.polarion.extension.pdf.exporter.util.placeholder.PlaceholderProcessor; import ch.sbb.polarion.extension.pdf.exporter.util.velocity.VelocityEvaluator; import com.polarion.alm.projects.IProjectService; @@ -100,9 +101,12 @@ void testConverter() { CoverPageProcessor coverPageProcessor = new CoverPageProcessor(placeholderProcessor, velocityEvaluator, weasyPrintConverter, coverPageSettings, new PdfTemplateProcessor()); + HtmlLinksHelper htmlLinksHelper = mock(HtmlLinksHelper.class); + when(htmlLinksHelper.internalizeLinks(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + ExportParams params = ExportParams.builder().projectId("test").locationPath("testLocation").orientation(Orientation.PORTRAIT).paperSize(PaperSize.A4).build(); PdfConverter converter = new PdfConverter(pdfExporterPolarionService, headerFooterSettings, cssSettings, liveDocHelper, placeholderProcessor, velocityEvaluator, - coverPageProcessor, weasyPrintConverter, new HtmlProcessor(null, localizationSettings), new PdfTemplateProcessor()); + coverPageProcessor, weasyPrintConverter, new HtmlProcessor(null, localizationSettings, htmlLinksHelper), new PdfTemplateProcessor()); compareContentUsingReferenceImages(testName + "_simple", converter.convertToPdf(params, null)); diff --git a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfWidthTest.java b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfWidthTest.java index 675752f..6c6e820 100644 --- a/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfWidthTest.java +++ b/src/test/java/ch/sbb/polarion/extension/pdf/exporter/weasyprint/PdfWidthTest.java @@ -21,7 +21,7 @@ public class PdfWidthTest extends BaseWeasyPrintTest { public static final String FIXED_POSTFIX = "Fixed"; - private static final HtmlProcessor htmlProcessor = new HtmlProcessor(null, null); + private static final HtmlProcessor htmlProcessor = new HtmlProcessor(null, null, null); private static Stream testWidthViolationParams() { return Stream.of(