]> git.argeo.org Git - gpl/argeo-suite.git/blob - org.argeo.app.servlet.publish/src/org/argeo/app/servlet/publish/DbkServlet.java
Improve build and deployment
[gpl/argeo-suite.git] / org.argeo.app.servlet.publish / src / org / argeo / app / servlet / 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.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;
55
56 import net.sf.saxon.BasicTransformerFactory;
57
58 /**
59 * A servlet transforming a dbk:* JCR node into HTML, using the DocBook XSL.
60 */
61 public class DbkServlet extends HttpServlet {
62 private static final long serialVersionUID = 6906020513498289335L;
63
64 private CmsLog log = CmsLog.getLog(DbkServlet.class);
65
66 private Repository repository;
67
68 private DocumentBuilderFactory documentBuilderFactory;
69 private TransformerFactory transformerFactory;
70 private Templates docBoookHtmlTemplates;
71
72 // FOP
73 private Templates docBoookFoTemplates;
74
75 private Map<String, CmsTheme> themes = Collections.synchronizedMap(new HashMap<>());
76
77 private String xslBase;
78
79 @Override
80 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
81
82 String pathInfo = req.getPathInfo();
83 if (pathInfo.startsWith("//"))
84 pathInfo = pathInfo.substring(1);
85 String path = URLDecoder.decode(pathInfo, StandardCharsets.UTF_8);
86
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);
93 if (cmsTheme == null)
94 throw new IllegalArgumentException("Theme " + themeId + " not found.");
95 resp.setContentType("text/css");
96 IOUtils.copy(cmsTheme.getResourceAsStream(cssPath), resp.getOutputStream());
97 return;
98 }
99
100 if (path.toLowerCase().endsWith("/index.html")) {
101 path = path.substring(0, path.length() - "/index.html".length());
102 }
103
104 boolean pdf = false;
105 if (path.toLowerCase().endsWith(".pdf")) {
106 pdf = true;
107 if (path.toLowerCase().endsWith("/index.pdf")) {
108 path = path.substring(0, path.length() - "/index.pdf".length());
109 }
110 }
111
112 Session session = null;
113 try {
114 session = RemoteAuthUtils.doAs(() -> Jcr.login(repository, null), new ServletHttpRequest(req));
115 Node node = session.getNode(path);
116
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.");
126 }
127
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");
131
132 // TODO optimise with pipes, SAX, etc. ?
133 byte[] arr;
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);
139 }
140
141 try (InputStream in = new ByteArrayInputStream(arr);) {
142
143 DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
144 Document doc = docBuilder.parse(in);
145 Source xmlInput = new DOMSource(doc);
146
147 if (pdf) {
148 // String baseUri = req.getRequestURI();
149 FopFactory fopFactory = FopFactory.newInstance(URI.create(req.getRequestURL().toString()));
150 resp.setContentType("application/pdf");
151
152 // // DocBook to FO
153 // byte[] foBytes;
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();
159 // }
160 //
161 // // FO to PDF
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);
168 // }
169 //
170
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);
175
176 } else {
177 Result xmlOutput = new StreamResult(resp.getOutputStream());
178 resp.setContentType("text/html");
179 Transformer docBookTransformer = getDocBoookHtmlTemplates().newTransformer();
180
181 // gather CSS
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(' ');
187 }
188 // FIXME make it more generic
189 sb.append("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap")
190 .append(' ');
191 sb.append(
192 "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,600;1,400&display=swap")
193 .append(' ');
194 if (sb.length() > 0)
195 docBookTransformer.setParameter("html.stylesheet", sb.toString());
196 }
197 docBookTransformer.transform(xmlInput, xmlOutput);
198 }
199 // resp.getOutputStream().write(out.toByteArray());
200 } catch (Exception e) {
201 throw new ServletException("Cannot transform " + path, e);
202 }
203 }
204 } else {
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");
215 } else {
216 // TODO know more content types...
217 resp.setHeader("Content-Disposition", "attachment; filename=\"" + node.getName() + "\"");
218 }
219 IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
220 } else {
221 throw new IllegalArgumentException("Unsupported node " + node);
222 }
223 }
224 } catch (RepositoryException e1) {
225 throw new JcrException(e1);
226 } finally {
227 Jcr.logout(session);
228 }
229 }
230
231 @Override
232 public void init() throws ServletException {
233
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.");
245 }
246 }
247 xslBase = defaultXslBase;
248
249 }
250
251 documentBuilderFactory = DocumentBuilderFactory.newInstance();
252 documentBuilderFactory.setXIncludeAware(true);
253 documentBuilderFactory.setNamespaceAware(true);
254
255 }
256
257 protected Templates createDocBookTemplates(String xsl) {
258 try {
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();
264 }
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);
269 return templates;
270 } catch (TransformerConfigurationException e) {
271 throw new IllegalStateException("Cannot instantiate XSL " + xsl, e);
272 }
273
274 }
275
276 protected Templates getDocBoookHtmlTemplates() {
277 if (docBoookHtmlTemplates == null) {
278 String htmlXsl = xslBase + "/html/docbook.xsl";
279 docBoookHtmlTemplates = createDocBookTemplates(htmlXsl);
280 }
281 return docBoookHtmlTemplates;
282 }
283
284 protected Templates getDocBoookFoTemplates() {
285 if (docBoookFoTemplates == null) {
286 String foXsl = xslBase + "/fo/docbook.xsl";
287 docBoookFoTemplates = createDocBookTemplates(foXsl);
288 }
289 return docBoookFoTemplates;
290 }
291
292 public void setRepository(Repository repository) {
293 this.repository = repository;
294 }
295
296 public void addTheme(CmsTheme theme, Map<String, String> properties) {
297 themes.put(theme.getThemeId(), theme);
298 }
299
300 public void removeTheme(CmsTheme theme, Map<String, String> properties) {
301 themes.remove(theme.getThemeId());
302 }
303
304 public static void main(String[] args) throws Exception {
305 // Step 1: Construct a FopFactory by specifying a reference to the configuration
306 // file
307 // (reuse if you plan to render multiple documents!)
308 FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
309
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")));
315
316 try {
317 // Step 3: Construct fop with desired output format
318 Fop fop = fopFactory.newFop("application/pdf", out);
319
320 // Step 4: Setup JAXP using identity transformer
321 TransformerFactory fopTransformerFactory = TransformerFactory.newInstance();
322 Transformer fopTransformer = fopTransformerFactory.newTransformer(); // identity transformer
323
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"));
328
329 // Resulting SAX events (the generated FO) must be piped through to FOP
330 Result res = new SAXResult(fop.getDefaultHandler());
331
332 // Step 6: Start XSLT transformation and FOP processing
333 fopTransformer.transform(src, res);
334
335 } finally {
336 // Clean-up
337 out.close();
338 }
339 }
340 }