I have a Spring application and need to build support for PDF generation. I'm thinking of using Flying-saucer together with Thymeleaf to render the PDF. However, I cannot find that much information about using Flying-saucer together with Thymeleaf. Have anyone else used those to technologies together?
I'm using Flyingsaucer-R8 with Thymeleaf 2.0.14 without problems (and I'm sure current version of Thymeleaf works as well).
I have separate TemplateEngine with classpath template resolver configured for this purpose. Using it to produce XHTML as String. Flyingsaucer creates PDF document from result then. Check example below.
Code below is example - NOT PRODUCTION ready code use it with NO WARRANTY. For sake of clarity there's no try-catch handling and no resources caching (creating PDF is quite expensive operation). Consider that.
Code
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.BaseFont;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
public class FlyingSoucerTestService {
public void test() throws DocumentException, IOException {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("META-INF/pdfTemplates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("XHTML");
templateResolver.setCharacterEncoding("UTF-8");
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
Context ctx = new Context();
ctx.setVariable("message", "I don't want to live on this planet anymore");
String htmlContent = templateEngine.process("messageTpl", ctx);
ByteOutputStream os = new ByteOutputStream();
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver fontResolver = renderer.getFontResolver();
ClassPathResource regular = new ClassPathResource("/META-INF/fonts/LiberationSerif-Regular.ttf");
fontResolver.addFont(regular.getURL().toString(), BaseFont.IDENTITY_H, true);
renderer.setDocumentFromString(htmlContent);
renderer.layout();
renderer.createPDF(os);
byte[] pdfAsBytes = os.getBytes();
os.close();
FileOutputStream fos = new FileOutputStream(new File("/tmp/message.pdf"));
fos.write(pdfAsBytes);
fos.close();
}
}
Template
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring3-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
div.border {
border: solid;
border-width: 1px 1px 0px 1px;
padding: 5px 20px 5px 20px;
}
</style>
</head>
<body style="font-family: Liberation Serif;">
<div class="border">
<h1 th:text="${message}">message</h1>
</div>
</body>
</html>
Related
I'm trying to generate a pdf contains Chinese UTF-8 characters via flying saucer and thymeleaf. But the generated pdf just ignore all the Chinese characters (latin is fine). Here is the Thymeleaf configuration
#Configuration
public class ThymeleafConfig {
#Bean
public ClassLoaderTemplateResolver fileTemplateResolver(){
ClassLoaderTemplateResolver fileTemplateResolver = new ClassLoaderTemplateResolver();
fileTemplateResolver.setPrefix("templates/");
fileTemplateResolver.setTemplateMode("HTML");
fileTemplateResolver.setSuffix(".html");
fileTemplateResolver.setCharacterEncoding("UTF-8");
fileTemplateResolver.setOrder(1);
return fileTemplateResolver;
}
#Bean
public SpringTemplateEngine templateEngine(){
SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
springTemplateEngine.setEnableSpringELCompiler(true);
springTemplateEngine.setTemplateResolver(fileTemplateResolver());
return springTemplateEngine;
}
#Bean
public ThymeleafViewResolver thymeleafViewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
}
As you can see, I've set the character encoding to UTF-8 for both template resolver and view resolver.
And the PDF Util for generating pdfs
public class PDFUtil {
#Autowired
private TemplateEngine templateEngine;
public String createPdf(String templatename, String fileName, String modelName, Object model) throws IOException, DocumentException {
String fileNameUrl = "";
Context ctx = new Context();
ctx.setVariable(modelName, model);
String processedHtml = templateEngine.process(templatename, ctx);
FileOutputStream outputStream = null;
try {
final File outputFile = File.createTempFile(fileName, ".pdf");
outputStream = new FileOutputStream(outputFile);
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
final ClassPathResource fonts = new ClassPathResource("fonts/PingFangSCRegular.ttf");
String test = fonts.getFilename();
resolver.addFont(fonts.getPath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
renderer.setDocumentFromString(processedHtml);
renderer.layout();
renderer.createPDF(outputStream, false);
renderer.finishPDF();
FileSystemResource resource = new FileSystemResource(outputFile);
fileNameUrl = resource.getURL().toString();
}
finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) { }
}
}
return fileNameUrl;
}
}
Here I've added the Chinese font to the resolver.
And this is the template html head
<head th:fragment="html_head">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Title</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/jsbarcode#latest/dist/JsBarcode.all.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/fontawesome.min.css" />
<style type="text/css">
#font-face {
font-family: 'PingFang SC Regular';
src: url('/fonts/PingFangSCRegular.ttf');
-fs-pdf-font-embed: embed;
-fs-pdf-font-encoding: Identity-H;
}
</style>
</head>
So I tried to declare that the charset is UTF-8 and the font family is PingFang SC Regular. But no surprise that does not work.
Here is the maven dependency I've added to my springboot project
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13.3</version>
</dependency>
The html body I want to render
<body>
<div th:fragment="header(model)">
<div class="row">
<div class="col-4">
<img id="barcode" alt="111"/>
</div>
<div class="col-4">
<h3>啊啊啊啊啊啊aaa[[${model.title}]]</h3>
<p id="print-time">[[${model.printTime}]]</p>
</div>
</div>
</div>
</body>
And the rendered result
Can anyone figure out why the UTF-8 character does not show in the generated pdf? Any idea would be appreciate.
In your template, you declare the font-face, but you don't apply it to the content of the page.
You just have to declare that the font should be used:
body {font-family: 'PingFang SC Regular';}
Also, you don't need to use #font-face in the template, as you have added the font to the renderer (using resolver.addFont).
The following HTML should work fine:
<html>
<head>
<style>
body {font-family: 'PingFang SC Regular';}
</style>
</head>
<body>
<h3>啊啊啊啊啊啊</h3>
</body>
</html>
Trying to configure a JAX-RS resource with #Path("/"), however, the resource is ignored and the first file found in resources is loaded.
Any idea how to prevent this and allow the resource to work?
When clearing META-INF/resources, the JAX-RS resource loads correctly.
Using:
Quarkus 1.4.2.Final
openjdk version "11.0.6" 2020-01-14 LTS
OpenJDK Runtime Environment Zulu11.37+52-SA (build 11.0.6+10-LTS)
OpenJDK 64-Bit Server VM Zulu11.37+52-SA (build 11.0.6+10-LTS, mixed mode)
Resource:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
#Path("/")
public class LandingResource {
#GET
#Produces(MediaType.TEXT_HTML)
public String getLandingPage() {
return "<html><head><title>Hello World</title></head><body>Hello!</body></html>";
}
}
Testing:
curl --location --request GET 'http://localhost:8080/'
Response:
<!doctype html>
<html lang="en">
<head>
<title>Internal Server Error - Error handling cee4cff3-551d-44e1-9102-5c9ada9d8fb2-7, java.nio.file.InvalidPathException: Illegal char <:> at index 97: <tempdir>\vertx-cache\file-cache-71fbfca9-5ba3-4a3e-8020-8501379cbf2b\<project dir>\src\main\resources\META-INF\resources\assets\icons\icon-128x128.png</title>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 100%;
font-weight: 100;
line-height: 1.4;
}
...
Achieved the desired outcome by adding a vertx web route:
import io.quarkus.vertx.web.Route;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import javax.enterprise.context.ApplicationScoped;
#ApplicationScoped
public class LandingRoute {
#Route(path = "/", methods = HttpMethod.GET)
public void landing(RoutingContext rc) {
rc.response().end("hello ");
}
}
In order to use #Route annotation you need to add quarkus-reactive-routes extension (io.quarkus:quarkus-reactive-routes) to project.
You can find more information about reactive routes in Quarkus documentation at:
https://quarkus.io/guides/reactive-routes
By default Quarkus will serve static resources from the root context.
That means that the resources inside src/main/resources/META-INF/resources/ are already mapped to root (http://localhost:8080/). This means that you can not map a standard JAX-RS on the root easily.
See the documentation for further information: https://quarkus.io/guides/http-reference
In your case you are returning a fixed HTML landing page. As a solution you could remove the LandingResource class and serve the landing page from the static resources.
This can be achieved by placing the HTML snippet in src/main/resources/META-INF/resources/index.html.
This is also how the default Quarkus default landing page is served.
I get this infamous error:
cannot be context relative (/) or page relative unless you implement
the IWebContext
I have a spring boot application (without the web module) that creates pdf files.
I am planning to use an HTML file as a template, but I could not link the css file nor the image properly due to these url issues.
Html :
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Company Invoice</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="./css/company.css" th:href="#{./css/company.css}"/>
</head>
<body>
<p th:utext="#{home.welcome}">Welcome !</p>
<img src="/images/gtvglogo.png" th:src="#{/images/gtvglogo.png}"/>
</body>
</html>
folder structure:
src/main/resources/templates/sample.html
src/main/resources/templates/css/sample.css
I googled a bit but I donT want to solve this via IWebContext.
Is there another way?
Thanks in advance.
org.thymeleaf.exceptions.TemplateProcessingException: Link base
"/a/relative/link" cannot be context
relative (/...) unless the context used for executing the engine
implements the org.thymeleaf.context.IWebContext interface (template:
"templates/a-template" - line 6, col 13)
1 at org.thymeleaf.linkbuilder.StandardLinkBuilder.computeContextPath
(StandardLinkBuilder.java:493)
...
The exception is thrown by the org.thymeleaf.linkbuilder.StandardLinkBuilder. By providing a different implementation of org.thymeleaf.linkbuilder.ILinkBuilder to the TemplateEngine we can avoid this expception
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setLinkBuilder(new ILinkBuilder() {
#Override
public String getName() {
return null;
}
#Override
public Integer getOrder() {
return null;
}
#Override
public String buildLink(IExpressionContext context, String base, Map<String, Object> parameters) {
return null;
}
});
I created an example Livy (Spark) application using the com.cloudera.livy.Job class for calculating an approximate value for Pi (Source: https://github.com/cloudera/livy#using-the-programmatic-api), exported as jar file to e.g. C:/path/to/the/pijob.jar.
Actually I'm running this job from another Main class like this (also copied from the link above and adapted):
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import com.cloudera.livy.LivyClient;
import com.cloudera.livy.LivyClientBuilder;
public class Main {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ExecutionException {
String livyUrl = "http://myserverIp:8998";
String piJar = "C:/path/to/the/pijob.jar";
int samples = 10000;
LivyClient client = new LivyClientBuilder().setURI(new URI(livyUrl)).build();
try {
System.err.printf("Uploading %s to the Spark context...\n", piJar);
client.uploadJar(new File(piJar)).get();
System.err.printf("Running PiJob with %d samples...\n", samples);
double pi = client.submit(new PiJob(samples)).get();
System.out.println("Pi is roughly: " + pi);
} finally {
client.stop(true);
}
}
}
This application works perfectly in an unsecured Hadoop cluster from outside (started from my client). But when I try to run it against a Kerberos-enabled Cluster, it fails.
I tried to set the corresponding Kerberos properties in the LivyClientBuilder class:
Properties props = new Properties();
props.put("livy.environment", "production");
props.put("livy.impersonation.enabled", "true");
props.put("livy.server.auth.kerberos.keytab", "/etc/security/keytabs/spnego.service.keytab");
props.put("livy.server.auth.kerberos.principal", "HTTP/_HOST#MYCLUSTER.DE");
props.put("livy.server.auth.type", "kerberos");
props.put("livy.server.csrf_protection.enabled", "true");
props.put("livy.server.kerberos.keytab", "/etc/security/keytabs/livy.service.keytab");
props.put("livy.server.kerberos.principal", "livy/_HOST#MYCLUSTER.DE");
props.put("livy.server.port", "8998");
props.put("livy.server.session.timeout", "3600000");
props.put("livy.superusers", "zeppelin-MyCluster");
LivyClient client = new LivyClientBuilder().setAll(props).setURI(new URI(livyUrl)).build();
But I still get an exception saying that authentication is required:
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Authentication required: <html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 401 </title>
</head>
<body>
<h2>HTTP ERROR: 401</h2>
<p>Problem accessing /sessions/. Reason:
<pre> Authentication required</pre></p>
<hr /><i><small>Powered by Jetty://</small></i>
</body>
</html>
at com.cloudera.livy.client.http.HttpClient.propagate(HttpClient.java:185)
at com.cloudera.livy.client.http.HttpClient.<init>(HttpClient.java:85)
at com.cloudera.livy.client.http.HttpClientFactory.createClient(HttpClientFactory.java:38)
at com.cloudera.livy.LivyClientBuilder.build(LivyClientBuilder.java:124)
at livy.Main.main(Main.java:34)
Caused by: java.io.IOException: Authentication required: <html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 401 </title>
</head>
<body>
<h2>HTTP ERROR: 401</h2>
<p>Problem accessing /sessions/. Reason:
<pre> Authentication required</pre></p>
<hr /><i><small>Powered by Jetty://</small></i>
</body>
</html>
at com.cloudera.livy.client.http.LivyConnection.sendRequest(LivyConnection.java:230)
at com.cloudera.livy.client.http.LivyConnection.sendJSONRequest(LivyConnection.java:204)
at com.cloudera.livy.client.http.LivyConnection.post(LivyConnection.java:180)
at com.cloudera.livy.client.http.HttpClient.<init>(HttpClient.java:82)
... 3 more
The questions at this point are for me:
Are these all needed Kerberos settings that I need?
Or do I have to add something more to log-in?
Do I have to provide config files / keytabs on my client machine?
or can I still use the server paths (like I did so far)?
Is there some helpful documentation on the Kerberos stuff for Livy?
I tried reading many topics but I found no answer.
Plz, some illuminated soul can help me?
The code are just 3 files, a very simple query using jsp.
NetBeans 8.1
Maven Web Application
Java EE 7 WEB
GlassFish 4.1.1
Source/Binary 1.8
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Exercício 01 JDBC</title>
</head>
<body>
<h1>Listagem de Usuário:</h1>
<FORM METHOD="POST" ACTION="App">
<P> Clique em <INPUT TYPE="SUBMIT" VALUE="LISTAR">
para obter a relação do nome do primeiro usuário.</p>
</FORM>
</body>
</html>
App.java
package br.com.yonathan.faculdades.jdbc;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletConfig;
#WebServlet(name = "App", urlPatterns = {"/App"})
public class App extends HttpServlet {
private Connection con;
#Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
final String url = "jdbc:oracle:thin:#yyy.inf.poa.ifrs.edu.br:1521:XE";
final String us = "xxx";
try {
Class.forName("oracle.jdbc.OracleDriver");
} catch (ClassNotFoundException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
}
try {
con = DriverManager.getConnection(url, us, us);
} catch (SQLException ex) {
throw new ServletException(ex);
}
}
#Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String sql = "SELECT nome FROM JDBC_USUARIO where ROWNUM = 1";
String saida = "";
try (PreparedStatement stm = con.prepareStatement(sql);
ResultSet rs = stm.executeQuery()) {
while (rs.next()) {
saida = rs.getString(1);
}
rs.close();
} catch (SQLException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
}
request.setAttribute("resultado", saida);
response.setContentType("text/html;charset=UTF-8");
request.getRequestDispatcher("resposta.jsp").forward(request, response);
}
}
resposta.jsp
<%#page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Servlet Exibe Resultado</title>
</head>
<body>
<h1>JDBC Connection</h1>
<h2>Primeiro nome cadastrado:</h2>
${resultado}.
</body>
</html>
The user and passwd are the same.
I really tried to fix before posting.
Look for ojdbc6.jar or ojdbc7.jar in your oracle install and put that file into WEB-INF/lib. Then restart tomcat and try again.
In the file "pom.xml" into Project Files, I had to add:
<dependencies>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
</dependencies>
and
<repositories>
<repository>
<id>codelds</id>
<url>https://code.lds.org/nexus/content/groups/main-repo</url>
</repository>
</repositories>
and it works.
I have no idea why doesn't works from the beginning. It's a Maven project, but I had to include "manually" this things and then when I click to run he download the missing driver and seems working well until now.
Thank you guys!