Skip to content

Commit

Permalink
feat: Added links internalization (#10)
Browse files Browse the repository at this point in the history
* feat: Added links internalization

Refs: #DEV-11660
  • Loading branch information
nirikash authored Jun 14, 2024
1 parent a3adba5 commit 8b75f41
Show file tree
Hide file tree
Showing 16 changed files with 321 additions and 10 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<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`:
```properties
ch.sbb.polarion.extension.pdf-exporter.internalizeExternalCss=true
```

## Extension Configuration

1. On the top of the project's navigation pane click ⚙ (Actions) ➙ 🔧 Administration. Project's administration page will be opened.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ch.sbb.polarion.extension.pdf.exporter.util.HtmlProcessor;
import ch.sbb.polarion.extension.pdf.exporter.util.PdfExporterFileResourceProvider;
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.weasyprint.WeasyPrintConverter;
import ch.sbb.polarion.extension.pdf.exporter.weasyprint.WeasyPrintConnectorFactory;
import ch.sbb.polarion.extension.pdf.exporter.weasyprint.WeasyPrintOptions;
Expand All @@ -26,10 +27,12 @@ public class HtmlToPdfConverter {

public HtmlToPdfConverter() {
this.pdfTemplateProcessor = new PdfTemplateProcessor();
this.htmlProcessor = new HtmlProcessor(new PdfExporterFileResourceProvider(), new LocalizationSettings());
PdfExporterFileResourceProvider fileResourceProvider = new PdfExporterFileResourceProvider();
this.htmlProcessor = new HtmlProcessor(fileResourceProvider, new LocalizationSettings(), new HtmlLinksHelper(fileResourceProvider));
this.weasyPrintConverter = WeasyPrintConnectorFactory.getWeasyPrintExecutor();
}

@VisibleForTesting
public HtmlToPdfConverter(PdfTemplateProcessor pdfTemplateProcessor, HtmlProcessor htmlProcessor, WeasyPrintConverter weasyPrintConverter) {
this.pdfTemplateProcessor = pdfTemplateProcessor;
this.htmlProcessor = htmlProcessor;
Expand Down Expand Up @@ -76,7 +79,10 @@ String preprocessHtml(String origHtml, Orientation orientation, PaperSize paperS
} else {
html = replaceTagContent(origHtml, "head", head);
}
return htmlProcessor.replaceImagesAsBase64Encoded(html);
html = htmlProcessor.replaceImagesAsBase64Encoded(html);
html = htmlProcessor.internalizeLinks(html);

return html;
}

private String extractTagContent(String html, String tag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import ch.sbb.polarion.extension.pdf.exporter.util.PdfExporterListStyleProvider;
import ch.sbb.polarion.extension.pdf.exporter.util.PdfGenerationLog;
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 ch.sbb.polarion.extension.pdf.exporter.weasyprint.WeasyPrintConverter;
Expand Down Expand Up @@ -65,7 +66,8 @@ public PdfConverter() {
velocityEvaluator = new VelocityEvaluator();
coverPageProcessor = new CoverPageProcessor();
weasyPrintConverter = WeasyPrintConnectorFactory.getWeasyPrintExecutor();
htmlProcessor = new HtmlProcessor(new PdfExporterFileResourceProvider(), new LocalizationSettings());
PdfExporterFileResourceProvider fileResourceProvider = new PdfExporterFileResourceProvider();
htmlProcessor = new HtmlProcessor(fileResourceProvider, new LocalizationSettings(), new HtmlLinksHelper(fileResourceProvider));
pdfTemplateProcessor = new PdfTemplateProcessor();
}

Expand Down Expand Up @@ -95,6 +97,7 @@ public byte[] convertToPdf(@NotNull ExportParams exportParams, @Nullable ExportM
if (metaInfoCallback != null) {
metaInfoCallback.setLinkedWorkItems(WorkItemRefData.extractListFromHtml(htmlContent, exportParams.getProjectId()));
}
htmlContent = htmlProcessor.internalizeLinks(htmlContent);

generationLog.log("Html is ready, starting pdf generation");
byte[] bytes = generatePdf(documentData, exportParams, metaInfoCallback, htmlContent, generationLog);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class PdfExporterExtensionConfiguration extends ExtensionConfiguration {
public static final String WEASYPRINT_EXECUTABLE = "weasyprint.executable";
public static final String WEASYPRINT_EXECUTABLE_DEFAULT = "weasyprint";
public static final String WEASYPRINT_PDF_VARIANT = "weasyprint.pdf.variant";
public static final String INTERNALIZE_EXTERNAL_CSS = "internalizeExternalCss";

public WeasyPrintConnector getWeasyprintConnector() {
String value = SystemValueReader.getInstance().readString(getPropertyPrefix() + WEASYPRINT_CONNECTOR, WEASYPRINT_CONNECTOR_DEFAULT);
Expand All @@ -37,13 +38,18 @@ public String getWeasyprintPdfVariant() {
return SystemValueReader.getInstance().readString(getPropertyPrefix() + WEASYPRINT_PDF_VARIANT, null);
}

public Boolean getInternalizeExternalCss() {
return SystemValueReader.getInstance().readBoolean(getPropertyPrefix() + INTERNALIZE_EXTERNAL_CSS, false);
}

@Override
public @NotNull List<String> getSupportedProperties() {
List<String> supportedProperties = new ArrayList<>(super.getSupportedProperties());
supportedProperties.add(WEASYPRINT_CONNECTOR);
supportedProperties.add(WEASYPRINT_SERVICE);
supportedProperties.add(WEASYPRINT_EXECUTABLE);
supportedProperties.add(WEASYPRINT_PDF_VARIANT);
supportedProperties.add(INTERNALIZE_EXTERNAL_CSS);
return supportedProperties;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ch.sbb.polarion.extension.pdf.exporter.rest.model.conversion.PaperSize;
import ch.sbb.polarion.extension.pdf.exporter.settings.LocalizationSettings;
import ch.sbb.polarion.extension.pdf.exporter.util.exporter.CustomPageBreakPart;
import ch.sbb.polarion.extension.pdf.exporter.util.html.HtmlLinksHelper;
import com.polarion.alm.shared.util.StringUtils;
import com.polarion.core.util.xml.CSSStyle;
import lombok.SneakyThrows;
Expand Down Expand Up @@ -146,10 +147,12 @@ public class HtmlProcessor {

private final FileResourceProvider fileResourceProvider;
private final LocalizationSettings localizationSettings;
private final HtmlLinksHelper httpLinksHelper;

public HtmlProcessor(FileResourceProvider fileResourceProvider, LocalizationSettings localizationSettings) {
public HtmlProcessor(FileResourceProvider fileResourceProvider, LocalizationSettings localizationSettings, HtmlLinksHelper httpLinksHelper) {
this.fileResourceProvider = fileResourceProvider;
this.localizationSettings = localizationSettings;
this.httpLinksHelper = httpLinksHelper;
}

public String processHtmlForPDF(@NotNull String html, @NotNull ExportParams exportParams, @NotNull List<String> selectedRoleEnumValues) {
Expand Down Expand Up @@ -469,7 +472,7 @@ private String filterByRoles(@NotNull String linkedWorkItems, @NotNull List<Stri
String role = matcher.group("role");
String roleSpan = matcher.group("roleSpan");
if (selectedRoleEnumValues.contains(role)) {
if (filteredContent.length() > 0) {
if (!filteredContent.isEmpty()) {
filteredContent.append(",<br>");
}
filteredContent.append(roleSpan);
Expand Down Expand Up @@ -1137,6 +1140,10 @@ public String replaceImagesAsBase64Encoded(String html) {
return html;
}

public String internalizeLinks(String html) {
return httpLinksHelper.internalizeLinks(html);
}

@VisibleForTesting
@SuppressWarnings("squid:S1166") // no need to log or rethrow exception by design
public byte[] processPossibleSvgImage(byte[] possibleSvgImageBytes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ch.sbb.polarion.extension.pdf.exporter.util.html;

import ch.sbb.polarion.extension.pdf.exporter.util.FileResourceProvider;

import java.util.Map;
import java.util.Optional;

public class ExternalCssInternalizer implements LinkInternalizer {
private static final String DATA_PRECEDENCE = "data-precedence";
private final FileResourceProvider fileResourceProvider;

public ExternalCssInternalizer(FileResourceProvider fileResourceProvider) {
this.fileResourceProvider = fileResourceProvider;
}

@Override
public Optional<String> inline(Map<String, String> attributes) {
if (!"stylesheet".equals(attributes.get("rel"))
|| !attributes.containsKey("href")) {
return Optional.empty();
}
StringBuilder inlinedContent = new StringBuilder("<style");
if (attributes.containsKey(DATA_PRECEDENCE)) {
inlinedContent.append(" ")
.append(DATA_PRECEDENCE)
.append("=\"")
.append(attributes.get(DATA_PRECEDENCE))
.append("\"");
}
inlinedContent.append(">");
String url = attributes.get("href");
inlinedContent.append(new String(fileResourceProvider.getResourceAsBytes(url)));
inlinedContent.append("</style>");

return Optional.of(inlinedContent.toString());
}
}
Original file line number Diff line number Diff line change
@@ -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("<link\\s*[^>]*>", Pattern.CASE_INSENSITIVE);
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("([\\w-]+)\\s*=\\s*(['\"])(.*?)\\2");

private final Set<LinkInternalizer> linkInliners;

public HtmlLinksHelper(FileResourceProvider fileResourceProvider) {
this (Set.of(
new ExternalCssInternalizer(fileResourceProvider)
));
}

public HtmlLinksHelper(Set<LinkInternalizer> 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<String, String> attributesMap = parseLinkTagAttributes(linkTag);
String replacement = inlineLinkTag(linkTag, attributesMap);

matcher.appendReplacement(newHtmlContent, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(newHtmlContent);

return newHtmlContent.toString();
}

public static Map<String, String> parseLinkTagAttributes(String linkTag) {
Map<String, String> 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<String, String> attributesMap) {
return linkInliners.stream()
.map(i -> i.inline(attributesMap))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse(linkTag);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> inline(Map<String, String> attributes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ <h4>WeasyPrint as Service in Docker</h4>
</code>
</pre>

<h4>Enabling Internalization of CSS Links</h4>

<p>The converting HTML can contain some external CSS links referencing Polarion Server, like:</p>
<pre>
<code data-language="html">
<span class="cm-tag cm-bracket">&lt;</span><span class="cm-tag">link</span> <span class="cm-attribute">rel</span>=<span class="cm-string">"stylesheet"</span> <span class="cm-attribute">href</span>=<span class="cm-string">"/polarion/diff-tool-app/ui/app/_next/static/css/3c374f9daffd361a.css"</span> <span class="cm-attribute">data-precedence</span>=<span class="cm-string">"next"</span><span class="cm-tag cm-bracket">&gt;</span>
</code>
</pre>
<p>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 &lt;link&gt; elements with internal CSS &lt;style&gt; 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 <code>&lt;POLARION_HOME&gt;/etc/polarion.properties</code>:</p>
<pre>
<code data-language="properties">
<span class="cm-def">ch.sbb.polarion.extension.pdf-exporter.internalizeExternalCss</span><span class=".cm-operator">=</span>true
</code>
</pre>


<h3>PDF exporter extension to appear on a Document's properties pane</h3>

<ol>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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("""
Expand Down Expand Up @@ -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("""
Expand Down Expand Up @@ -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("""
Expand Down Expand Up @@ -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("""
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.*;

Expand All @@ -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<String, String> deTranslations = Map.of(
"draft", "Entwurf",
"not reviewed", "Nicht überprüft"
Expand Down
Loading

0 comments on commit 8b75f41

Please sign in to comment.