1 package org
.argeo
.app
.servlet
.publish
;
3 import java
.io
.BufferedOutputStream
;
4 import java
.io
.ByteArrayInputStream
;
5 import java
.io
.ByteArrayOutputStream
;
7 import java
.io
.FileOutputStream
;
8 import java
.io
.IOException
;
9 import java
.io
.InputStream
;
10 import java
.io
.OutputStream
;
12 import java
.net
.URLDecoder
;
13 import java
.nio
.charset
.StandardCharsets
;
14 import java
.nio
.file
.Files
;
15 import java
.nio
.file
.Paths
;
16 import java
.util
.Collections
;
17 import java
.util
.HashMap
;
20 import javax
.jcr
.Node
;
21 import javax
.jcr
.Repository
;
22 import javax
.jcr
.RepositoryException
;
23 import javax
.jcr
.Session
;
24 import javax
.jcr
.nodetype
.NodeType
;
25 import javax
.servlet
.ServletException
;
26 import javax
.servlet
.http
.HttpServlet
;
27 import javax
.servlet
.http
.HttpServletRequest
;
28 import javax
.servlet
.http
.HttpServletResponse
;
29 import javax
.xml
.parsers
.DocumentBuilder
;
30 import javax
.xml
.parsers
.DocumentBuilderFactory
;
31 import javax
.xml
.transform
.Result
;
32 import javax
.xml
.transform
.Source
;
33 import javax
.xml
.transform
.Templates
;
34 import javax
.xml
.transform
.Transformer
;
35 import javax
.xml
.transform
.TransformerConfigurationException
;
36 import javax
.xml
.transform
.TransformerFactory
;
37 import javax
.xml
.transform
.dom
.DOMSource
;
38 import javax
.xml
.transform
.sax
.SAXResult
;
39 import javax
.xml
.transform
.stream
.StreamResult
;
40 import javax
.xml
.transform
.stream
.StreamSource
;
42 import org
.apache
.commons
.io
.IOUtils
;
43 import org
.apache
.fop
.apps
.Fop
;
44 import org
.apache
.fop
.apps
.FopFactory
;
45 import org
.argeo
.api
.cms
.CmsLog
;
46 import org
.argeo
.api
.cms
.ux
.CmsTheme
;
47 import org
.argeo
.app
.docbook
.DbkType
;
48 import org
.argeo
.app
.jcr
.docbook
.DbkJcrUtils
;
49 import org
.argeo
.cms
.auth
.RemoteAuthUtils
;
50 import org
.argeo
.cms
.servlet
.ServletHttpRequest
;
51 import org
.argeo
.jcr
.Jcr
;
52 import org
.argeo
.jcr
.JcrException
;
53 import org
.argeo
.jcr
.JcrUtils
;
54 import org
.w3c
.dom
.Document
;
56 import net
.sf
.saxon
.BasicTransformerFactory
;
59 * A servlet transforming a dbk:* JCR node into HTML, using the DocBook XSL.
61 public class DbkServlet
extends HttpServlet
{
62 private static final long serialVersionUID
= 6906020513498289335L;
64 private CmsLog log
= CmsLog
.getLog(DbkServlet
.class);
66 private Repository repository
;
68 private DocumentBuilderFactory documentBuilderFactory
;
69 private TransformerFactory transformerFactory
;
70 private Templates docBoookHtmlTemplates
;
73 private Templates docBoookFoTemplates
;
75 private Map
<String
, CmsTheme
> themes
= Collections
.synchronizedMap(new HashMap
<>());
77 private String xslBase
;
80 protected void doGet(HttpServletRequest req
, HttpServletResponse resp
) throws ServletException
, IOException
{
82 String pathInfo
= req
.getPathInfo();
83 if (pathInfo
.startsWith("//"))
84 pathInfo
= pathInfo
.substring(1);
85 String path
= URLDecoder
.decode(pathInfo
, StandardCharsets
.UTF_8
);
87 if (path
.toLowerCase().endsWith(".css")) {
88 path
= path
.substring(1);
89 int firstSlash
= path
.indexOf('/');
90 String themeId
= path
.substring(0, firstSlash
);
91 String cssPath
= path
.substring(firstSlash
);
92 CmsTheme cmsTheme
= themes
.get(themeId
);
94 throw new IllegalArgumentException("Theme " + themeId
+ " not found.");
95 resp
.setContentType("text/css");
96 IOUtils
.copy(cmsTheme
.getResourceAsStream(cssPath
), resp
.getOutputStream());
100 if (path
.toLowerCase().endsWith("/index.html")) {
101 path
= path
.substring(0, path
.length() - "/index.html".length());
105 if (path
.toLowerCase().endsWith(".pdf")) {
107 if (path
.toLowerCase().endsWith("/index.pdf")) {
108 path
= path
.substring(0, path
.length() - "/index.pdf".length());
112 Session session
= null;
114 session
= RemoteAuthUtils
.doAs(() -> Jcr
.login(repository
, null), new ServletHttpRequest(req
));
115 Node node
= session
.getNode(path
);
117 if (node
.hasNode(DbkType
.article
.get())) {
118 Node dbkNode
= node
.getNode(DbkType
.article
.get());
119 if (DbkJcrUtils
.isDbk(dbkNode
)) {
120 CmsTheme cmsTheme
= null;
121 String themeId
= req
.getParameter("themeId");
122 if (themeId
!= null) {
123 cmsTheme
= themes
.get(themeId
);
124 if (cmsTheme
== null)
125 throw new IllegalArgumentException("Theme " + themeId
+ " not found.");
128 // TODO customise DocBook so that it outputs UTF-8
129 // see http://www.sagehill.net/docbookxsl/OutputEncoding.html
130 resp
.setContentType("text/html; charset=ISO-8859-1");
132 // TODO optimise with pipes, SAX, etc. ?
134 try (ByteArrayOutputStream out
= new ByteArrayOutputStream()) {
135 session
.exportDocumentView(dbkNode
.getPath(), out
, true, false);
136 arr
= out
.toByteArray();
137 } catch (RepositoryException e
) {
138 throw new JcrException(e
);
141 try (InputStream in
= new ByteArrayInputStream(arr
);) {
143 DocumentBuilder docBuilder
= documentBuilderFactory
.newDocumentBuilder();
144 Document doc
= docBuilder
.parse(in
);
145 Source xmlInput
= new DOMSource(doc
);
148 // String baseUri = req.getRequestURI();
149 FopFactory fopFactory
= FopFactory
.newInstance(URI
.create(req
.getRequestURL().toString()));
150 resp
.setContentType("application/pdf");
154 // try (ByteArrayOutputStream out = new ByteArrayOutputStream();) {
155 // Result xmlOutput = new StreamResult(out);
156 // Transformer docBookTransformer = docBoookFoTemplates.newTransformer();
157 // docBookTransformer.transform(xmlInput, xmlOutput);
158 // foBytes = out.toByteArray();
162 // try (InputStream foIn = new ByteArrayInputStream(foBytes)) {
163 // Fop fop = fopFactory.newFop("application/pdf", resp.getOutputStream());
164 // Transformer fopTransformer = transformerFactory.newTransformer(); // identity
165 // Source src = new StreamSource(foIn);
166 // Result fopResult = new SAXResult(fop.getDefaultHandler());
167 // fopTransformer.transform(src, fopResult);
171 Fop fop
= fopFactory
.newFop("application/pdf", resp
.getOutputStream());
172 Transformer docBookTransformer
= getDocBoookFoTemplates().newTransformer();
173 Result fopResult
= new SAXResult(fop
.getDefaultHandler());
174 docBookTransformer
.transform(xmlInput
, fopResult
);
177 Result xmlOutput
= new StreamResult(resp
.getOutputStream());
178 resp
.setContentType("text/html");
179 Transformer docBookTransformer
= getDocBoookHtmlTemplates().newTransformer();
182 if (cmsTheme
!= null) {
183 StringBuilder sb
= new StringBuilder();
184 for (String cssPath
: cmsTheme
.getWebCssPaths()) {
185 sb
.append(req
.getContextPath()).append(req
.getServletPath()).append('/');
186 sb
.append(themeId
).append('/').append(cssPath
).append(' ');
188 // FIXME make it more generic
189 sb
.append("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap")
192 "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,600;1,400&display=swap")
195 docBookTransformer
.setParameter("html.stylesheet", sb
.toString());
197 docBookTransformer
.transform(xmlInput
, xmlOutput
);
199 // resp.getOutputStream().write(out.toByteArray());
200 } catch (Exception e
) {
201 throw new ServletException("Cannot transform " + path
, e
);
205 if (node
.isNodeType(NodeType
.NT_FILE
)) {// media download etc.
206 String fileNameLowerCase
= node
.getName().toLowerCase();
207 if (fileNameLowerCase
.endsWith(".jpg") || fileNameLowerCase
.endsWith(".jpeg")) {
208 resp
.setContentType("image/jpeg");
209 } else if (fileNameLowerCase
.endsWith(".png")) {
210 resp
.setContentType("image/png");
211 } else if (fileNameLowerCase
.endsWith(".gif")) {
212 resp
.setContentType("image/gif");
213 } else if (fileNameLowerCase
.endsWith(".svg")) {
214 resp
.setContentType("image/svg+xml");
216 // TODO know more content types...
217 resp
.setHeader("Content-Disposition", "attachment; filename=\"" + node
.getName() + "\"");
219 IOUtils
.copy(JcrUtils
.getFileAsStream(node
), resp
.getOutputStream());
221 throw new IllegalArgumentException("Unsupported node " + node
);
224 } catch (RepositoryException e1
) {
225 throw new JcrException(e1
);
232 public void init() throws ServletException
{
234 // TODO improve configuration and provisioning of DocBook XSL
235 xslBase
= System
.getProperty("argeo.docbook.xsl");
236 if (xslBase
== null) {
237 // We need namespace aware XSL!
238 // Fedora (sudo dnf install docbook5-style-xsl)
239 String defaultXslBase
= "/usr/share/xml/docbook/stylesheet/docbook-xsl-ns/";
240 if (!Files
.exists(Paths
.get(defaultXslBase
))) {
241 defaultXslBase
= "/opt/docbook-xsl";
242 if (!Files
.exists(Paths
.get(defaultXslBase
))) {
243 log
.error("System property argeo.docbook.xsl is not set and default location " + defaultXslBase
244 + " does not exist.");
247 xslBase
= defaultXslBase
;
251 documentBuilderFactory
= DocumentBuilderFactory
.newInstance();
252 documentBuilderFactory
.setXIncludeAware(true);
253 documentBuilderFactory
.setNamespaceAware(true);
257 protected Templates
createDocBookTemplates(String xsl
) {
259 if (transformerFactory
== null) {
260 transformerFactory
= new BasicTransformerFactory();
261 // We must explicitly use the non-XSLTC transformer, as XSLTC is not working
262 // with DocBook stylesheets
263 // transformerFactory = new TransformerFactoryImpl();
265 Source xslSource
= new StreamSource(xsl
);
266 Templates templates
= transformerFactory
.newTemplates(xslSource
);
267 if (templates
== null)
268 throw new IllegalStateException("Could not instantiate XSL " + xsl
);
270 } catch (TransformerConfigurationException e
) {
271 throw new IllegalStateException("Cannot instantiate XSL " + xsl
, e
);
276 protected Templates
getDocBoookHtmlTemplates() {
277 if (docBoookHtmlTemplates
== null) {
278 String htmlXsl
= xslBase
+ "/html/docbook.xsl";
279 docBoookHtmlTemplates
= createDocBookTemplates(htmlXsl
);
281 return docBoookHtmlTemplates
;
284 protected Templates
getDocBoookFoTemplates() {
285 if (docBoookFoTemplates
== null) {
286 String foXsl
= xslBase
+ "/fo/docbook.xsl";
287 docBoookFoTemplates
= createDocBookTemplates(foXsl
);
289 return docBoookFoTemplates
;
292 public void setRepository(Repository repository
) {
293 this.repository
= repository
;
296 public void addTheme(CmsTheme theme
, Map
<String
, String
> properties
) {
297 themes
.put(theme
.getThemeId(), theme
);
300 public void removeTheme(CmsTheme theme
, Map
<String
, String
> properties
) {
301 themes
.remove(theme
.getThemeId());
304 public static void main(String
[] args
) throws Exception
{
305 // Step 1: Construct a FopFactory by specifying a reference to the configuration
307 // (reuse if you plan to render multiple documents!)
308 FopFactory fopFactory
= FopFactory
.newInstance(new File(".").toURI());
310 // Step 2: Set up output stream.
311 // Note: Using BufferedOutputStream for performance reasons (helpful with
312 // FileOutputStreams).
313 OutputStream out
= new BufferedOutputStream(new FileOutputStream(new File(
314 System
.getProperty("user.home") + "/dev/git/unstable/argeo-qa/doc/platform/argeo-platform-test.pdf")));
317 // Step 3: Construct fop with desired output format
318 Fop fop
= fopFactory
.newFop("application/pdf", out
);
320 // Step 4: Setup JAXP using identity transformer
321 TransformerFactory fopTransformerFactory
= TransformerFactory
.newInstance();
322 Transformer fopTransformer
= fopTransformerFactory
.newTransformer(); // identity transformer
324 // Step 5: Setup input and output for XSLT transformation
325 // Setup input stream
326 Source src
= new StreamSource(new File(
327 System
.getProperty("user.home") + "/dev/git/unstable/argeo-qa/doc/platform/argeo-platform.fo"));
329 // Resulting SAX events (the generated FO) must be piped through to FOP
330 Result res
= new SAXResult(fop
.getDefaultHandler());
332 // Step 6: Start XSLT transformation and FOP processing
333 fopTransformer
.transform(src
, res
);