Skip to content

Instantly share code, notes, and snippets.

@massahud
Last active June 28, 2017 14:15
Show Gist options
  • Save massahud/5067019 to your computer and use it in GitHub Desktop.
Save massahud/5067019 to your computer and use it in GitHub Desktop.
flyingsaucer xhtml to pdf servlet filter

Filter to generate PDF from Servlet dynamic pages

This filter intercepts the response and runs Flying Saucer ITextRenderer on it, returning a pdf instead.

Configuration

Just put the filter on your code and configure the url patterns where it will run on web.xml. The filtered pages will return as pdf documents.

If your webserver does not resolve locally to 'localhost', change line #60 to a address that resolves to itself.

Maven dependency:

<dependency>
  <groupId>com.lowagie</groupId>
  <artifactId>itext</artifactId>
  <version>2.1.7</version>
</dependency>
<dependency>
  <groupId>org.xhtmlrenderer</groupId>
  <artifactId>flying-saucer-pdf</artifactId>
  <version>9.0.9</version>
</dependency>

Page

The page can be configured using the CSS3 page rule (http://www.w3.org/TR/css3-page/)

Example:

@page {
  size: A4 portrait;
  @bottom-right {
    content: "Page " counter(page) " of " counter(pages);
  };
}

Header and footer

Simple headers and footers can be generated using the content property on the @page rule, but you can also use html elements. To use HTML elements, you must use the CSS3 Generated Content For Paged Media module (http://www.w3.org/TR/css3-gcpm/)

Basically, declare an html element with the value running(nameOfReference) on the position property and, inside the @page rule, call the element using element(nameOfReference).

Example:

HTML:

<div id="footer" >
  Page <span class="page"/> de <span class="pages"/>
</div>

CSS:

#footer {
  position: running(footer);
  font-weight: bold;
  font-size: 9pt;
}

@page {
  size: A4 portrait;
  @bottom-right {
    content: element(footer)
  };
}

span.page:before {
  content: counter(page);
}

span.pages:before {
  content: counter(pages);
}

Filtro para gerar PDF de páginas dinâmicas do servlet

Esse filtro intercepta a resposta e executa o ITextRenderer do Flying Saucer, retornando um pdf no lugar do XHTML gerado.

Configuração

Simplesmente coloque o filtro no seu código e configure os url patterns no web.xml. As páginas que ele filtrar serão retornadas como documentos pdf.

Se o seu webserver não resolver localmente 'localhost', modifiquea linha #60 do .java para um endereço que o webserver resolva para ele mesmo.

Dependência Maven:

<dependency>
  <groupId>com.lowagie</groupId>
  <artifactId>itext</artifactId>
  <version>2.1.7</version>
</dependency>
<dependency>
  <groupId>org.xhtmlrenderer</groupId>
  <artifactId>flying-saucer-pdf</artifactId>
  <version>9.0.9</version>
</dependency>

Página

A página pode ser configurada usando o CSS3 Page Rule (http://www.w3.org/TR/css3-page/)

Exemplo:

@page {
  size: A4 portrait;
  @bottom-right {
    content: "Página " counter(page) " de " counter(pages);
  };
}

Cabeçalho e rodapé

Cabeçalhos e rodapés simples podem ser gerados usando a propriedade content nas regras @page, como no exemplo anterior, mas também é possível utilizar elementos html.

Para usar elementos html, deve-se utilizar o módulo CSS3 Generated Content For Paged Media (http://www.w3.org/TR/css3-gcpm/)

Basicamente, declare um elemento html com o valor running(nomeDeReferencia) na propriedade position, e dentro da regra @page, chame o elemento usando element(nomeDeReferencia).

Exemplo:

HTML::

<div id="footer" >
  Página <span class="pagina"/> de <span class="paginas"/>
</div>

CSS:

#footer {
  position: running(footer);
  font-weight: bold;
  font-size: 9pt;
}

@page {
  size: A4 portrait;
  @bottom-right {
    content: element(footer)
  };
}

span.pagina:before {
  content: counter(page);
}

span.paginas:before {
  content: counter(pages);
}
package com.massahud.web.filter.pdf;
import com.lowagie.text.DocumentException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.resource.FSEntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Filtro que captura a resposta da aplicação e gera um pdf caso o parâmetro
* media com valor pdf exista na requisição. Aceita também o parâmetro filename,
* que faz com que o browser salve o arquivo com o nome informado ao invés de
* criar um nome à partir da URL.
*/
public class FiltroPdfRenderer implements Filter {
private static final Logger logger = Logger.getLogger(FiltroPdfRenderer.class);
public static final String NOME_PARAMETRO_MEDIA = "media";
public static final String MEDIA_PDF = "pdf";
public static final String NOME_PARAMETRO_ARQUIVO = "filename";
private DocumentBuilderFactory factory;
private ServletContext ctx;
@Override
public void init(FilterConfig config) throws ServletException {
// System.getProperties().setProperty("xr.util-logging.loggingEnabled", "true");
// XRLog.setLoggingEnabled(true);
ctx = config.getServletContext();
factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
try {
factory.setFeature("http://xml.org/sax/features/validation", false);
factory.setFeature("http://xml.org/sax/features/namespaces", false);
} catch (ParserConfigurationException ex) {
logger.error("Erro ao inicializar filtro pdf: " + ex.getMessage(), ex);
}
try {
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (ParserConfigurationException ex) {
logger.error("Erro ao inicializar filtro pdf: " + ex.getMessage(), ex);
}
factory.setIgnoringComments(true);
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain filterChain) throws IOException, ServletException {
// Aplica o filtro apenas se o parâmetro media for pdf
if (!MEDIA_PDF.equalsIgnoreCase(req.getParameter(NOME_PARAMETRO_MEDIA))) {
filterChain.doFilter(req, resp);
} else if (req instanceof HttpServletRequest && resp instanceof HttpServletResponse) {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) resp;
//Capture the content for this request
final ContentCaptureServletResponse capContent = new ContentCaptureServletResponse(response);
filterChain.doFilter(request, capContent);
try {
// Gera o URI do documento, para que o renderer saiba os
// endereços relativos de recursos de css e imagens
final URL urlDocumento = new URL("http", "localhost", request.getLocalPort(), request.getRequestURI());
// Se o parâmetro filename for passado, coloca no cabeçalho respectivo o nome informado
String filename = request.getParameter(NOME_PARAMETRO_ARQUIVO);
if (filename == null) {
final String contextPath = request.getContextPath().replaceAll(".*/", "");
filename = contextPath + ".pdf";
}
// Parse do HTML gerado para um documento legível pelo XHTML renderer
final StringReader contentReader = new StringReader(capContent.getContent());
final InputSource source = new InputSource(contentReader);
source.setPublicId(filename);
final ITextRenderer renderer = parse(source, urlDocumento);
response.setContentType("application/pdf");
filename = URLEncoder.encode(filename, "UTF-8");
response.setHeader("Content-Disposition", "filename=\"" + filename + "\"");
try (OutputStream browserStream = response.getOutputStream()) {
renderer.createPDF(browserStream);
} catch (DocumentException e) {
throw new ServletException(e);
}
} catch (SAXException e) {
throw new ServletException(e);
}
}
}
@Override
public void destroy() {
}
private ITextRenderer parse(InputSource source, URL urlDocumento) throws SAXException, IOException, ServletException {
try {
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
documentBuilder.setEntityResolver(FSEntityResolver.instance());
final Document xhtmlContent = documentBuilder.parse(source);
final ITextRenderer renderer = new ITextRenderer();
renderer.setDocument(xhtmlContent, urlDocumento.toExternalForm());
JSFUserAgentCallback userAgent = new JSFUserAgentCallback(renderer.getOutputDevice(), ctx);
userAgent.setBaseURL(urlDocumento.toExternalForm());
userAgent.setSharedContext(renderer.getSharedContext());
renderer.layout();
return renderer;
} catch (ParserConfigurationException ex) {
throw new ServletException(ex);
}
}
private static class ContentCaptureServletResponse extends HttpServletResponseWrapper {
private ByteArrayOutputStream contentBuffer;
private PrintWriter writer;
ContentCaptureServletResponse(HttpServletResponse originalResponse) {
super(originalResponse);
contentBuffer = new ByteArrayOutputStream();
try {
writer = new PrintWriter(new OutputStreamWriter(contentBuffer, "UTF-8"), true);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
@Override
public PrintWriter getWriter() throws IOException {
if (writer == null) {
contentBuffer = new ByteArrayOutputStream();
writer = new PrintWriter(new OutputStreamWriter(contentBuffer, "UTF-8"), true);
}
return writer;
}
public String getContent() {
try {
writer.flush();
final String xhtmlContent = new String(contentBuffer.toByteArray(), "UTF-8");
return xhtmlContent;
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
}
package com.massahud.web.filter.pdf;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import org.apache.log4j.Logger;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextOutputDevice;
import org.xhtmlrenderer.pdf.ITextUserAgent;
public class JSFUserAgentCallback extends ITextUserAgent {
private static final Logger logger = Logger.getLogger(JSFUserAgentCallback.class);
private final ServletContext ctx;
public JSFUserAgentCallback(ITextOutputDevice outputDevice, ServletContext ctx) {
super(outputDevice);
this.ctx = ctx;
}
private final Pattern pattern = Pattern.compile("^.*/javax\\.faces\\.resource/(.+)\\.jsf\\?ln=([^&]+).*$");
@Override
protected InputStream resolveAndOpenStream(String uri) {
logger.debug("resolveAndOpenStream - " + uri);
java.io.InputStream is = null;
URL url = null;
try {
url = new URL(uri);
} catch (MalformedURLException ex) {
logger.error(ex);
}
if (url == null) {
return super.resolveAndOpenStream(uri);
}
try {
is = url.openStream();
} catch (IOException ex) {
logger.warn("IO problem for " + uri, ex);
return super.resolveAndOpenStream(uri);
}
return is;
}
@Override
public String resolveURI(String uri) {
Matcher matcher = pattern.matcher(uri);
if (matcher.matches()) {
try {
// procura resource em webapp/resources
String relativo = "/resources/" + matcher.group(2) + "/" + matcher.group(1);
URL url = ctx.getResource(relativo);
if (url == null) {
// procura resource em META-INF/resources de algum jar
url = ctx.getClassLoader().getResource("/META-INF" + relativo);
}
if (url != null) {
URI uriGerada = url.toURI();
uri = uriGerada.toString();
return uri;
} else {
logger.warn("Recurso " + uri + " não encontrado.");
}
} catch (MalformedURLException | URISyntaxException ex) {
logger.error("Exceção ao tentar resolver uri recurso: " + ex.getMessage(), ex);
}
}
return super.resolveURI(uri);
}
@Override
public void setSharedContext(SharedContext sharedContext) {
super.setSharedContext(sharedContext);
sharedContext.setUserAgentCallback(this);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment