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(