package org.argeo.publishing.servlet; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.Map; import javax.jcr.Node; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NodeType; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Templates; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.apache.commons.io.IOUtils; import org.apache.xalan.processor.TransformerFactoryImpl; import org.argeo.cms.servlet.ServletAuthUtils; import org.argeo.cms.ui.CmsTheme; import org.argeo.docbook.DbkType; import org.argeo.docbook.DbkUtils; import org.argeo.jcr.Jcr; import org.argeo.jcr.JcrException; import org.argeo.jcr.JcrUtils; import org.w3c.dom.Document; /** * A servlet transforming a dbk:* JCR node into HTML, using the DocBook XSL. */ public class DbkServlet extends HttpServlet { private static final long serialVersionUID = 6906020513498289335L; private Repository repository; private DocumentBuilderFactory documentBuilderFactory; private TransformerFactory transformerFactory; private Templates docBoookTemplates; private Map themes = Collections.synchronizedMap(new HashMap<>()); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pathInfo = req.getPathInfo(); if (pathInfo.startsWith("//")) pathInfo = pathInfo.substring(1); String path = URLDecoder.decode(pathInfo, StandardCharsets.UTF_8); if (path.toLowerCase().endsWith(".css")) { path = path.substring(1); int firstSlash = path.indexOf('/'); String themeId = path.substring(0, firstSlash); String cssPath = path.substring(firstSlash); CmsTheme cmsTheme = themes.get(themeId); if (cmsTheme == null) throw new IllegalArgumentException("Theme " + themeId + " not found."); resp.setContentType("text/css"); IOUtils.copy(cmsTheme.getResourceAsStream(cssPath), resp.getOutputStream()); return; } Session session = null; try { session = ServletAuthUtils.doAs(() -> Jcr.login(repository, null), req); Node documentNode = session.getNode(path); Node node; if (documentNode.hasNode(DbkType.article.get())) node = documentNode.getNode(DbkType.article.get()); else { throw new IllegalArgumentException("Unsupported node " + documentNode); } if (DbkUtils.isDbk(node)) { CmsTheme cmsTheme = null; String themeId = req.getParameter("themeId"); if (themeId != null) { cmsTheme = themes.get(themeId); if (cmsTheme == null) throw new IllegalArgumentException("Theme " + themeId + " not found."); } // TODO customise DocBook so that it outputs UTF-8 // see http://www.sagehill.net/docbookxsl/OutputEncoding.html resp.setContentType("text/html; charset=ISO-8859-1"); // TODO optimise with pipes, SAX, etc. ? byte[] arr; try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { session.exportDocumentView(node.getPath(), out, true, false); arr = out.toByteArray(); // System.out.println(new String(arr, StandardCharsets.UTF_8)); } catch (RepositoryException e) { throw new JcrException(e); } try (InputStream in = new ByteArrayInputStream(arr); // ByteArrayOutputStream out = new ByteArrayOutputStream(); ) { Result xmlOutput = new StreamResult(resp.getOutputStream()); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); // Document doc = docBuilder.parse(new File( // System.getProperty("user.home") + "/dev/git/gpl/argeo-qa/doc/platform/argeo-platform.dbk.xml")); Document doc = docBuilder.parse(in); Source xmlInput = new DOMSource(doc); Transformer transformer = docBoookTemplates.newTransformer(); // gather CSS if (cmsTheme != null) { StringBuilder sb = new StringBuilder(); for (String cssPath : cmsTheme.getWebCssPaths()) { sb.append(req.getContextPath()).append(req.getServletPath()).append('/'); sb.append(themeId).append('/').append(cssPath).append(' '); } sb.append("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap") .append(' '); sb.append( "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,600;1,400&display=swap") .append(' '); if (sb.length() > 0) transformer.setParameter("html.stylesheet", sb.toString()); } transformer.transform(xmlInput, xmlOutput); // resp.getOutputStream().write(out.toByteArray()); } catch (Exception e) { throw new ServletException("Cannot transform " + path, e); } } else if (node.isNodeType(NodeType.NT_FILE)) {// media download etc. String fileNameLowerCase = node.getName().toLowerCase(); if (fileNameLowerCase.endsWith(".jpg") || fileNameLowerCase.endsWith(".jpeg")) { resp.setContentType("image/jpeg"); } else if (fileNameLowerCase.endsWith(".png")) { resp.setContentType("image/png"); } else if (fileNameLowerCase.endsWith(".gif")) { resp.setContentType("image/gif"); } else if (fileNameLowerCase.endsWith(".svg")) { resp.setContentType("image/svg+xml"); } else { // TODO know more content types... resp.setHeader("Content-Disposition", "attachment; filename=\"" + node.getName() + "\""); } IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream()); } else { throw new IllegalArgumentException("Unsupported node " + node); } } catch (RepositoryException e1) { throw new JcrException(e1); } finally { Jcr.logout(session); } } @Override public void init() throws ServletException { // TODO improve configuration and provisioning of DocBook XSL String xslBase = System.getProperty("argeo.docbook.xsl"); if (xslBase == null) { String defaultXslBase = "/opt/docbook-xsl"; if (!Files.exists(Paths.get(defaultXslBase))) { throw new ServletException("System property argeo.docbook.xsl is not set and default location " + defaultXslBase + " does not exist."); } else { xslBase = defaultXslBase; } } String xsl = xslBase + "/html/docbook.xsl"; documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setXIncludeAware(true); documentBuilderFactory.setNamespaceAware(true); // We must explicitly use the non-XSLTC transformer, as XSLTC is not working // with DocBook stylesheets transformerFactory = new TransformerFactoryImpl(); Source xslSource = new StreamSource(xsl); try { docBoookTemplates = transformerFactory.newTemplates(xslSource); if (docBoookTemplates == null) throw new ServletException("Could not instantiate XSL " + xsl); } catch (TransformerConfigurationException e) { throw new ServletException("Cannot instantiate XSL " + xsl, e); } } public void setRepository(Repository repository) { this.repository = repository; } public void addTheme(CmsTheme theme, Map properties) { themes.put(theme.getThemeId(), theme); } public void removeTheme(CmsTheme theme, Map properties) { themes.remove(theme.getThemeId()); } }