]> git.argeo.org Git - gpl/argeo-suite.git/blob - publish/DbkServlet.java
Prepare next development cycle
[gpl/argeo-suite.git] / publish / DbkServlet.java
1 package org.argeo.app.servlet.publish;
2
3 import java.io.BufferedOutputStream;
4 import java.io.ByteArrayInputStream;
5 import java.io.ByteArrayOutputStream;
6 import java.io.File;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.OutputStream;
11 import java.net.URI;
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;
18 import java.util.Map;
19
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;
41
42 import org.apache.commons.io.IOUtils;
43 import org.apache.fop.apps.Fop;
44 import org.apache.fop.apps.FopFactory;
45 import org.apache.xalan.processor.TransformerFactoryImpl;
46 import org.argeo.api.cms.CmsLog;
47 import org.argeo.api.cms.ux.CmsTheme;
48 import org.argeo.app.docbook.DbkType;
49 import org.argeo.app.docbook.DbkUtils;
50 import org.argeo.cms.auth.RemoteAuthUtils;
51 import org.argeo.cms.servlet.ServletHttpRequest;
52 import org.argeo.jcr.Jcr;
53 import org.argeo.jcr.JcrException;
54 import org.argeo.jcr.JcrUtils;
55 import org.w3c.dom.Document;
56
57 /**
58 * A servlet transforming a dbk:* JCR node into HTML, using the DocBook XSL.
59 */
60 public class DbkServlet extends HttpServlet {
61 private static final long serialVersionUID = 6906020513498289335L;
62
63 private CmsLog log = CmsLog.getLog(DbkServlet.class);
64
65 private Repository repository;
66
67 private DocumentBuilderFactory documentBuilderFactory;
68 private TransformerFactory transformerFactory;
69 private Templates docBoookHtmlTemplates;
70
71 // FOP
72 private Templates docBoookFoTemplates;
73
74 private Map<String, CmsTheme> themes = Collections.synchronizedMap(new HashMap<>());
75
76 private String xslBase;
77
78 @Override
79 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
80
81 String pathInfo = req.getPathInfo();
82 if (pathInfo.startsWith("//"))
83 pathInfo = pathInfo.substring(1);
84 String path = URLDecoder.decode(pathInfo, StandardCharsets.UTF_8);
85
86 if (path.toLowerCase().endsWith(".css")) {
87 path = path.substring(1);
88 int firstSlash = path.indexOf('/');
89 String themeId = path.substring(0, firstSlash);
90 String cssPath = path.substring(firstSlash);
91 CmsTheme cmsTheme = themes.get(themeId);
92 if (cmsTheme == null)
93 throw new IllegalArgumentException("Theme " + themeId + " not found.");
94 resp.setContentType("text/css");
95 IOUtils.copy(cmsTheme.getResourceAsStream(cssPath), resp.getOutputStream());
96 return;
97 }
98
99 if (path.toLowerCase().endsWith("/index.html")) {
100 path = path.substring(0, path.length() - "/index.html".length());
101 }
102
103 boolean pdf = false;
104 if (path.toLowerCase().endsWith(".pdf")) {
105 pdf = true;
106 if (path.toLowerCase().endsWith("/index.pdf")) {
107 path = path.substring(0, path.length() - "/index.pdf".length());
108 }
109 }
110
111 Session session = null;
112 try {
113 session = RemoteAuthUtils.doAs(() -> Jcr.login(repository, null), new ServletHttpRequest(req));
114 Node node = session.getNode(path);
115
116 if (node.hasNode(DbkType.article.get())) {
117 Node dbkNode = node.getNode(DbkType.article.get());
118 if (DbkUtils.isDbk(dbkNode)) {
119 CmsTheme cmsTheme = null;
120 String themeId = req.getParameter("themeId");
121 if (themeId != null) {
122 cmsTheme = themes.get(themeId);
123 if (cmsTheme == null)
124 throw new IllegalArgumentException("Theme " + themeId + " not found.");
125 }
126
127 // TODO customise DocBook so that it outputs UTF-8
128 // see http://www.sagehill.net/docbookxsl/OutputEncoding.html
129 resp.setContentType("text/html; charset=ISO-8859-1");
130
131 // TODO optimise with pipes, SAX, etc. ?
132 byte[] arr;
133 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
134 session.exportDocumentView(dbkNode.getPath(), out, true, false);
135 arr = out.toByteArray();
136 } catch (RepositoryException e) {
137 throw new JcrException(e);
138 }
139
140 try (InputStream in = new ByteArrayInputStream(arr);) {
141
142 DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
143 Document doc = docBuilder.parse(in);
144 Source xmlInput = new DOMSource(doc);
145
146 if (pdf) {
147 // String baseUri = req.getRequestURI();
148 FopFactory fopFactory = FopFactory.newInstance(URI.create(req.getRequestURL().toString()));
149 resp.setContentType("application/pdf");
150
151 // // DocBook to FO
152 // byte[] foBytes;
153 // try (ByteArrayOutputStream out = new ByteArrayOutputStream();) {
154 // Result xmlOutput = new StreamResult(out);
155 // Transformer docBookTransformer = docBoookFoTemplates.newTransformer();
156 // docBookTransformer.transform(xmlInput, xmlOutput);
157 // foBytes = out.toByteArray();
158 // }
159 //
160 // // FO to PDF
161 // try (InputStream foIn = new ByteArrayInputStream(foBytes)) {
162 // Fop fop = fopFactory.newFop("application/pdf", resp.getOutputStream());
163 // Transformer fopTransformer = transformerFactory.newTransformer(); // identity
164 // Source src = new StreamSource(foIn);
165 // Result fopResult = new SAXResult(fop.getDefaultHandler());
166 // fopTransformer.transform(src, fopResult);
167 // }
168 //
169
170 Fop fop = fopFactory.newFop("application/pdf", resp.getOutputStream());
171 Transformer docBookTransformer = getDocBoookFoTemplates().newTransformer();
172 Result fopResult = new SAXResult(fop.getDefaultHandler());
173 docBookTransformer.transform(xmlInput, fopResult);
174
175 } else {
176 Result xmlOutput = new StreamResult(resp.getOutputStream());
177 resp.setContentType("text/html");
178 Transformer docBookTransformer = getDocBoookHtmlTemplates().newTransformer();
179
180 // gather CSS
181 if (cmsTheme != null) {
182 StringBuilder sb = new StringBuilder();
183 for (String cssPath : cmsTheme.getWebCssPaths()) {
184 sb.append(req.getContextPath()).append(req.getServletPath()).append('/');
185 sb.append(themeId).append('/').append(cssPath).append(' ');
186 }
187 // FIXME make it more generic
188 sb.append("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap")
189 .append(' ');
190 sb.append(
191 "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,600;1,400&display=swap")
192 .append(' ');
193 if (sb.length() > 0)
194 docBookTransformer.setParameter("html.stylesheet", sb.toString());
195 }
196 docBookTransformer.transform(xmlInput, xmlOutput);
197 }
198 // resp.getOutputStream().write(out.toByteArray());
199 } catch (Exception e) {
200 throw new ServletException("Cannot transform " + path, e);
201 }
202 }
203 } else {
204 if (node.isNodeType(NodeType.NT_FILE)) {// media download etc.
205 String fileNameLowerCase = node.getName().toLowerCase();
206 if (fileNameLowerCase.endsWith(".jpg") || fileNameLowerCase.endsWith(".jpeg")) {
207 resp.setContentType("image/jpeg");
208 } else if (fileNameLowerCase.endsWith(".png")) {
209 resp.setContentType("image/png");
210 } else if (fileNameLowerCase.endsWith(".gif")) {
211 resp.setContentType("image/gif");
212 } else if (fileNameLowerCase.endsWith(".svg")) {
213 resp.setContentType("image/svg+xml");
214 } else {
215 // TODO know more content types...
216 resp.setHeader("Content-Disposition", "attachment; filename=\"" + node.getName() + "\"");
217 }
218 IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
219 } else {
220 throw new IllegalArgumentException("Unsupported node " + node);
221 }
222 }
223 } catch (RepositoryException e1) {
224 throw new JcrException(e1);
225 } finally {
226 Jcr.logout(session);
227 }
228 }
229
230 @Override
231 public void init() throws ServletException {
232
233 // TODO improve configuration and provisioning of DocBook XSL
234 xslBase = System.getProperty("argeo.docbook.xsl");
235 if (xslBase == null) {
236 // We need namespace aware XSL!
237 // Fedora (sudo dnf install docbook5-style-xsl)
238 String defaultXslBase = "/usr/share/xml/docbook/stylesheet/docbook-xsl-ns/";
239 if (!Files.exists(Paths.get(defaultXslBase))) {
240 defaultXslBase = "/opt/docbook-xsl";
241 if (!Files.exists(Paths.get(defaultXslBase))) {
242 log.error("System property argeo.docbook.xsl is not set and default location " + defaultXslBase
243 + " does not exist.");
244 }
245 }
246 xslBase = defaultXslBase;
247
248 }
249
250 documentBuilderFactory = DocumentBuilderFactory.newInstance();
251 documentBuilderFactory.setXIncludeAware(true);
252 documentBuilderFactory.setNamespaceAware(true);
253
254 }
255
256 protected Templates createDocBookTemplates(String xsl) {
257 try {
258 if (transformerFactory == null) {
259 // We must explicitly use the non-XSLTC transformer, as XSLTC is not working
260 // with DocBook stylesheets
261 transformerFactory = new TransformerFactoryImpl();
262 }
263 Source xslSource = new StreamSource(xsl);
264 Templates templates = transformerFactory.newTemplates(xslSource);
265 if (templates == null)
266 throw new IllegalStateException("Could not instantiate XSL " + xsl);
267 return templates;
268 } catch (TransformerConfigurationException e) {
269 throw new IllegalStateException("Cannot instantiate XSL " + xsl, e);
270 }
271
272 }
273
274 protected Templates getDocBoookHtmlTemplates() {
275 if (docBoookHtmlTemplates == null) {
276 String htmlXsl = xslBase + "/html/docbook.xsl";
277 docBoookHtmlTemplates = createDocBookTemplates(htmlXsl);
278 }
279 return docBoookHtmlTemplates;
280 }
281
282 protected Templates getDocBoookFoTemplates() {
283 if (docBoookFoTemplates == null) {
284 String foXsl = xslBase + "/fo/docbook.xsl";
285 docBoookFoTemplates = createDocBookTemplates(foXsl);
286 }
287 return docBoookFoTemplates;
288 }
289
290 public void setRepository(Repository repository) {
291 this.repository = repository;
292 }
293
294 public void addTheme(CmsTheme theme, Map<String, String> properties) {
295 themes.put(theme.getThemeId(), theme);
296 }
297
298 public void removeTheme(CmsTheme theme, Map<String, String> properties) {
299 themes.remove(theme.getThemeId());
300 }
301
302 public static void main(String[] args) throws Exception {
303 // Step 1: Construct a FopFactory by specifying a reference to the configuration
304 // file
305 // (reuse if you plan to render multiple documents!)
306 FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
307
308 // Step 2: Set up output stream.
309 // Note: Using BufferedOutputStream for performance reasons (helpful with
310 // FileOutputStreams).
311 OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(
312 System.getProperty("user.home") + "/dev/git/unstable/argeo-qa/doc/platform/argeo-platform-test.pdf")));
313
314 try {
315 // Step 3: Construct fop with desired output format
316 Fop fop = fopFactory.newFop("application/pdf", out);
317
318 // Step 4: Setup JAXP using identity transformer
319 TransformerFactory fopTransformerFactory = TransformerFactory.newInstance();
320 Transformer fopTransformer = fopTransformerFactory.newTransformer(); // identity transformer
321
322 // Step 5: Setup input and output for XSLT transformation
323 // Setup input stream
324 Source src = new StreamSource(new File(
325 System.getProperty("user.home") + "/dev/git/unstable/argeo-qa/doc/platform/argeo-platform.fo"));
326
327 // Resulting SAX events (the generated FO) must be piped through to FOP
328 Result res = new SAXResult(fop.getDefaultHandler());
329
330 // Step 6: Start XSLT transformation and FOP processing
331 fopTransformer.transform(src, res);
332
333 } finally {
334 // Clean-up
335 out.close();
336 }
337 }
338 }