diff --git a/apachefop-serverless-az-func/.vscode/settings.json b/apachefop-serverless-az-func/.vscode/settings.json index 435acfd..c9a9b2b 100644 --- a/apachefop-serverless-az-func/.vscode/settings.json +++ b/apachefop-serverless-az-func/.vscode/settings.json @@ -7,7 +7,9 @@ "java.configuration.updateBuildConfiguration": "automatic", "maven.view": "hierarchical", "cSpell.words": [ + "apachefop", "Gzipped", + "Keepin", "xslfo" ] } \ No newline at end of file diff --git a/apachefop-serverless-az-func/host.json b/apachefop-serverless-az-func/host.json index 4ac8957..244d121 100644 --- a/apachefop-serverless-az-func/host.json +++ b/apachefop-serverless-az-func/host.json @@ -1,7 +1,7 @@ { - "version": "2.0", - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[1.*, 2.0.0)" - } + "version": "2.0"//, + // "extensionBundle": { + // "id": "Microsoft.Azure.Functions.ExtensionBundle", + // "version": "[1.*, 2.0.0)" + // } } \ No newline at end of file diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderResult.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderResult.java index 66f6aa0..a1b4e22 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderResult.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderResult.java @@ -7,7 +7,7 @@ public class ApacheFopRenderResult { private final ApacheFopEventListener eventListener; public ApacheFopRenderResult(byte[] pdfBytes, ApacheFopEventListener eventListener) { - this.pdfBytes = pdfBytes; + this.pdfBytes = pdfBytes != null ? pdfBytes : new byte[0]; this.eventListener = eventListener; } diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderer.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderer.java index 32b8ced..d985933 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderer.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderer.java @@ -3,6 +3,10 @@ import com.cajuncoding.apachefop.serverless.config.ApacheFopServerlessConfig; import com.cajuncoding.apachefop.serverless.config.ApacheFopServerlessConstants; import com.cajuncoding.apachefop.serverless.utils.ResourceUtils; +import com.cajuncoding.apachefop.serverless.utils.XPathUtils; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.fop.apps.*; import org.apache.fop.configuration.ConfigurationException; import org.apache.fop.configuration.DefaultConfigurationBuilder; @@ -11,6 +15,7 @@ import javax.xml.transform.sax.SAXResult; import javax.xml.transform.stream.StreamSource; import java.io.*; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.logging.Logger; import java.util.zip.GZIPOutputStream; @@ -93,23 +98,32 @@ protected synchronized void initApacheFopFactorySafely() { var configFilePath = ApacheFopServerlessConstants.ConfigXmlResourceName; FopFactory newFopFactory = null; - try (var configStream = ResourceUtils.loadResourceAsStream(configFilePath);) { - if (configStream != null) { + try { + String configXmlText = ResourceUtils.loadResourceAsString(configFilePath); + if (StringUtils.isNotBlank(configXmlText)) { //When Debugging log the full Configuration file... if(this.apacheFopConfig.isDebuggingEnabled()) { - var configFileXmlText =ResourceUtils.loadResourceAsString(configFilePath); - LogMessage("[DEBUG] ApacheFOP Configuration Xml:".concat(System.lineSeparator()).concat(configFileXmlText)); + LogMessage("[DEBUG] ApacheFOP Configuration Xml:".concat(System.lineSeparator()).concat(configXmlText)); } //Attempt to initialize with Configuration loaded from Configuration XML Resource file... - var cfgBuilder = new DefaultConfigurationBuilder(); - var cfg = cfgBuilder.build(configStream); - - var fopFactoryBuilder = new FopFactoryBuilder(baseUri, fopResourcesFileResolver).setConfiguration(cfg); - + //NOTE: FOP Factory requires a Stream so we have to initialize a new Stream for it to load from! + FopFactoryBuilder fopFactoryBuilder; + try(var configXmlStream = IOUtils.toInputStream(configXmlText, StandardCharsets.UTF_8)) { + var cfgBuilder = new DefaultConfigurationBuilder(); + var cfg = cfgBuilder.build(configXmlStream); + + fopFactoryBuilder = new FopFactoryBuilder(baseUri, fopResourcesFileResolver).setConfiguration(cfg); + } //Ensure Accessibility is programmatically set (default configuration is false)... - //fopFactoryBuilder.setAccessibility(this.apacheFopConfig.isAccessibilityPdfRenderingEnabled()); + //NOTE: There appears to be a bug in ApacheFOP code or documentation whereby it does not load the value from Xml as defined in the Docs! + // to work around this we read the value ourselves and also provide convenience support to simply set it in Azure Functions Configuration + // and if either configuration value is true then it will be enabled. + //NOTE: The XPathUtils is null safe so any issues in loading/parsing will simply result in null or default values... + var configXml = XPathUtils.fromXml(configXmlText); + var isAccessibilityEnabledInXmlConfig = configXml.evalXPathAsBoolean("//fop/accessibility", false); + fopFactoryBuilder.setAccessibility(this.apacheFopConfig.isAccessibilityPdfRenderingEnabled() || isAccessibilityEnabledInXmlConfig); newFopFactory = fopFactoryBuilder.build(); } diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/config/ApacheFopServerlessConfig.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/config/ApacheFopServerlessConfig.java index 8079bee..32c58d6 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/config/ApacheFopServerlessConfig.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/config/ApacheFopServerlessConfig.java @@ -83,7 +83,7 @@ protected void readRequestQueryParamsConfig(Map queryParams) { //Determine if Event Log Dump mode is enabled (vs PDF Binary return). this.eventLogDumpModeEnabled = BooleanUtils.toBoolean( - queryParams.getOrDefault(ApacheFopServerlessQueryParams.EventLogDump, null) + queryParams.getOrDefault(ApacheFopServerlessQueryParams.EventLogDump, null) ); } diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/TextUtils.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/TextUtils.java index 6a83b56..597732b 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/TextUtils.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/TextUtils.java @@ -18,10 +18,6 @@ public static String getCurrentW3cDateTime() { return currentW3cDateTime; } - public static boolean isNullOrWhiteSpace(String value) { - return value == null || value.isBlank(); - } - /** * Truncates a string to the number of characters that fit in X bytes avoiding multi byte characters being cut in * half at the cut off point. Also handles surrogate pairs where 2 characters in the string is actually one literal diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/XPathUtils.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/XPathUtils.java new file mode 100644 index 0000000..e699898 --- /dev/null +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/XPathUtils.java @@ -0,0 +1,94 @@ +package com.cajuncoding.apachefop.serverless.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.io.IOUtils; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class XPathUtils { + + public static XPathUtils fromXml(String xmlContent) + { + try (var xmlContentStream = IOUtils.toInputStream(xmlContent, StandardCharsets.UTF_8)) { + return XPathUtils.fromXml(xmlContentStream); + } catch (IOException e) { + e.printStackTrace(); + } + + return new XPathUtils(null); + } + + public static XPathUtils fromXml(InputStream xmlContentStream) + { + Document xmlDocument = null; + try { + xmlDocument = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(xmlContentStream); + } catch (SAXException | IOException | ParserConfigurationException e) { + e.printStackTrace(); + } + + return new XPathUtils(xmlDocument); + } + + protected Document _xmlDoc; + + public XPathUtils(Document xmlDoc) + { + _xmlDoc = xmlDoc; + } + + public Document getXmlDocument() { + return _xmlDoc; + } + + public T evalXPath(String xpath, Class classType) throws XPathExpressionException + { + return evalXPathInternal(xpath, classType); + } + + public String evalXPathAsString(String xpath, String defaultIfNotFound) throws XPathExpressionException + { + try { + var result = evalXPathInternal(xpath, String.class); + return result != null ? result : defaultIfNotFound; + } catch (XPathExpressionException e) { + return defaultIfNotFound; + } + } + + public boolean evalXPathAsBoolean(String xpath, boolean defaultIfNotFound) + { + try { + var result = evalXPathInternal(xpath, Boolean.class); + return (boolean)(result != null ? result : defaultIfNotFound); + } catch (XPathExpressionException e) { + return defaultIfNotFound; + } + } + + public T evalXPathInternal(String xpathCommand, Class classType) throws XPathExpressionException + { + if(_xmlDoc == null) + return null; + + XPath xpath = XPathFactory.newInstance().newXPath(); + // //XPathExpression xpathExpression = xpathCompiler.compile(xpath); + // var result = xpathExpression.evaluate(_xmlDoc, returnType); + var result = xpath.evaluateExpression(xpathCommand, _xmlDoc, classType); + return result; + } +} + diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/ApacheFopServerlessFunctionExecutor.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/ApacheFopServerlessFunctionExecutor.java index 02c24cc..8c8d92b 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/ApacheFopServerlessFunctionExecutor.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/ApacheFopServerlessFunctionExecutor.java @@ -92,6 +92,9 @@ protected HttpResponseMessage ExecuteRequestInternal( //Execute the transformation of the XSL-FO source content to Binary PDF format... var pdfRenderResult = fopHelper.renderPdfResult(xslFOBodyContent, config.isGzipResponseEnabled()); + //Add some contextual Logging so we can know if the PDF bytes were rendered... + logger.info(MessageFormat.format("[SUCCESS] Successfully Rendered PDF with [{0}] bytes.", pdfRenderResult.getPdfBytes().length)); + //Render the PDF Response (or EventLog Dump if specified)... var response = config.isEventLogDumpModeEnabled() ? responseBuilder.BuildEventLogDumpResponse(pdfRenderResult, config) diff --git a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/SafeHeader.java b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/SafeHeader.java index 7f0678a..410c995 100644 --- a/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/SafeHeader.java +++ b/apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/web/SafeHeader.java @@ -1,7 +1,8 @@ package com.cajuncoding.apachefop.serverless.web; import com.cajuncoding.apachefop.serverless.http.HttpEncodings; -import com.cajuncoding.apachefop.serverless.utils.TextUtils; + +import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import java.io.UnsupportedEncodingException; @@ -19,10 +20,10 @@ public SafeHeader(String value, String encoding) throws UnsupportedEncodingExcep public String getValue() { return value; } protected String sanitizeTextForHttpHeader(String value, String encoding) throws UnsupportedEncodingException { - if(TextUtils.isNullOrWhiteSpace(value)) + if(StringUtils.isBlank(value)) return value; - if(encoding != null && encoding != HttpEncodings.IDENTITY_ENCODING) + if(StringUtils.isNotBlank(encoding) && !encoding.equalsIgnoreCase(HttpEncodings.IDENTITY_ENCODING)) return value; //BBernard - 09/29/2021 diff --git a/apachefop-serverless-az-func/src/main/resources/apache-fop-config.xml b/apachefop-serverless-az-func/src/main/resources/apache-fop-config.xml index d70703e..6f52b04 100644 --- a/apachefop-serverless-az-func/src/main/resources/apache-fop-config.xml +++ b/apachefop-serverless-az-func/src/main/resources/apache-fop-config.xml @@ -23,6 +23,9 @@ true + + false +