com.seanox.pdf.Service Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of seanox-pdf-service Show documentation
Show all versions of seanox-pdf-service Show documentation
PDF service for generating/rendering PDFs based on Open HTML to PDF
/**
* LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt, im
* Folgenden Seanox Software Solutions oder kurz Seanox genannt.
* Diese Software unterliegt der Version 2 der Apache License.
*
* PDF Service
* Copyright (C) 2020 Seanox Software Solutions
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.seanox.pdf;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.multipdf.Splitter;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.seanox.pdf.Service.Template.Resources;
import com.seanox.pdf.Service.Template.TemplateException;
/**
* Static service for creating PDF based on templates and meta-objects.
*
* Examples of use:
*
* Service.generate(template, meta);
*
* Files.write(new File(template + ".pdf").toPath(), Service.generate(template, meta), StandardOpenOption.CREATE);
*
*
* How it works:
* The creation of PDFs is based on an HTML-to-PDF converter (openhtmltopdf).
* In the first step, an HTML is created that contains all data records.
* The HTML is based on a markup template with placeholders. A generator or
* renderer fills the placeholders in the template and creates an
* Single-Page-HTML as text.
* The HTML-to-PDF converter creates the PDF from the HTML.
* The pages are separated by CSS.
* The service works data neutral. There is no special data object.
* Only the key value entries in the maps and the placeholders in the template
* determine the content.
*
* Useful information:
* Templates are based on an implementation of the {@link Template} and the
* annoation {@link Resources}, which with {@link Resources#base()} and
* {@link Resources#template()}) contains information about the base directory
* of the resources (stylesheets, images, fonts, ...), as well the path of the
* markup template with the same name.
* It is practical if the template implementation, the markup template and any
* template extensions (properties, ...) are stored in the same package.
*
* The resources (stylesheets, images, fonts, ...) use HTML-to-PDF from the
* ClassPath, means the base URI required by the HTML-to-PDF converter refers to
* the ClassPath of this class. The location in the ClassPath can be defined
* with {@link Resources#base()}.
*
* About the templates
* The template implementation takes over the rendering of the templates.
* The implementation decides which generator, renderer, engine, ... it uses.
* As engine {@link Generator} is used, here you can find more details.
* The most important in short form:
*
* #[palceholder]
* Simple placeholder, global or in a section.
*
* #[section[[...]]]
* Section/Bock can contain more substructures.
* Sections/blocks are only rendered if a corresponding map entry exists.
*
* #[locale]
* Placeholder provided by {@link Service} with the current language.
* Available in all sections (header, content/data, footer).
*
* #[page]
* Placeholder provided by {@link Service} with the current page number.
* Available in sections: header, footer
*
* #[pages]
* Placeholder provided by {@link Service} with the total page number.
* Available in sections: header, footer
*
* Service 3.5.1 20200316
* Copyright (C) 2020 Seanox Software Solutions
* Alle Rechte vorbehalten.
*
* @author Seanox Software Solutions
* @version 3.5.1 20200316
*/
public class Service {
/**
* Creates a PDF for a template and data as meta object.
* @param template {@link Template}
* @param meta {@link Meta}
* @return the created PDF as byte array
* @throws TemplateException
* In case of unexpected errors.
* @throws ServiceException
* In case of unexpected errors.
*/
public static byte[] generate(Class extends Template> template, Meta meta)
throws ServiceException {
Template instance;
try {instance = Template.class.newInstance();
} catch (Exception exception) {
throw new Template.TemplateException(exception);
}
try {return Service.generate(instance, meta);
} catch (Exception exception) {
throw new ServiceException(exception);
}
}
/**
* Creates a PDF for a template and data as meta object.
* @param template {@link Template}
* @param meta {@link Meta}
* @return the created PDF as byte array
* @throws ServiceException
* In case of unexpected errors.
*/
public static byte[] generate(Template template, Meta meta)
throws ServiceException {
try {new URI(template.getBase().toString());
} catch (Exception exception) {
throw new Template.TemplateException("Invalid base URI", exception);
}
if (meta == null)
meta = new Meta();
try {return template.generate(meta);
} catch (Exception exception) {
throw new ServiceException(exception);
}
}
/**
* Meta object for creating PDFs.
* The PDF creation is based on templates and is decoupled from the business
* logic. The templates only know placeholders and structures.
* Templates consist of the parts: header, data and footer.
* These are three map objects that contain keys and values. The values can
* be strings or maps and collections with deeper structures, comparable to
* JSON as a nested data structure.
*/
public static class Meta {
/** locale */
private Locale locale;
/** key-value map for the header */
private Map header;
/** key-value map for the data */
private Map data;
/** key-value map for the footer */
private Map footer;
/** key-value map for the static texts */
private Map statics;
/** Constructor, creates a new Meta object. */
public Meta() {
}
/**
* Return value of locale.
* @return value of locale
*/
public Locale getLocale() {
return this.locale;
}
/**
* Set value of locale.
* @param locale value of locale
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
/**
* Return value of header.
* @return value of header
*/
public Map getHeader() {
return this.header;
}
/**
* Set value of header.
* @param header value of header
*/
public void setHeader(Map header) {
this.header = header;
}
/**
* Return value of data.
* @return value of data
*/
public Map getData() {
return this.data;
}
/**
* Set value of data.
* @param data value of data
*/
public void setData(Map data) {
this.data = data;
}
/**
* Return value of statics.
* @return value of statics
*/
public Map getStatics() {
return this.statics;
}
/**
* Set value of statics.
* @param statics value of statics
*/
public void setStatics(Map statics) {
this.statics = statics;
}
/**
* Return value of footer.
* @return value of footer
*/
public Map getFooter() {
return this.footer;
}
/**
* Set value of footer.
* @param footer value of footer
*/
public void setFooter(Map footer) {
this.footer = footer;
}
@Override
protected Object clone() {
Meta meta = new Meta();
meta.data = this.data;
meta.footer = this.footer;
meta.header = this.header;
meta.locale = this.locale;
meta.statics = this.statics;
return meta;
}
/**
* Meta-Type is required during generation so that the corresponding
* meta data (Key-Value Map) is used for the markup.
*/
public static enum Type {
/** Meta-Type HEADER */
HEADER,
/** Meta-Type DATA */
DATA,
/** Meta-Type FOOTER */
FOOTER
}
}
/** Abstract class for implementing templates. */
public static abstract class Template {
/**
* Templates are based on an implementation of the
* {@link Template} and the annoation {@link Resources}, which with
* {@link Resources#base()} and {@link Resources#template()}) contains
* information about the base directory of the resources (stylesheets,
* images, fonts, ...), as well the path of the markup template with the
* same name.
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resources {
/**
* Base URI of the resources in the ClassPath.
* Default value is the root in the ClassPath.
*/
String base() default "/";
/**
* Path of the markup template in the ClassPath.
* Default value is path of the class in the ClassPath with the
* extension 'html'.
*/
String template() default "";
}
/** Array of template implementations detected in the ClassPath */
private static Class[] templates;
/**
* Detects all template implementations in the ClassPath.
* The detection is based on using the annotation {@link Resources} and
* the implementation of {@link Template}.
* The detection is time-consuming and is therefore only executed once
* at runtime and the result is cached.
* @return the detected template implementations as an array
* @throws Exception
* In case of unexpected errors.
*/
@SuppressWarnings("unchecked")
public static Class[] scan()
throws Exception {
if (Template.templates != null)
return Template.templates.clone();
List> templates = new ArrayList<>();
for (String basePackage : Stream.of(new Throwable().getStackTrace())
.map(element -> element.getClassName().replaceAll("\\W.*$", ""))
.distinct()
.toArray(String[]::new)) {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(Resources.class));
for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {
Class> template = Class.forName(beanDefinition.getBeanClassName());
if (Template.class.isAssignableFrom(template))
templates.add((Class)template);
}
}
Template.templates = templates.toArray(new Class[0]);
return Template.templates.clone();
}
/**
* Returns the resources path (stylesheets, images, fonts, ...)
* @return the resources path
*/
protected String getBasePath() {
Resources resource = this.getClass().getAnnotation(Resources.class);
if (resource == null
|| resource.base().trim().isEmpty())
return "/";
return resource.base().trim();
}
/**
* Returns the URI of resources path (stylesheets, images, fonts, ...)
* @return the URI of resources path
* @throws TemplateResourceNotFoundException
* If the resources path cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected URI getBase()
throws Exception {
return this.getResource(this.getBasePath());
}
/**
* Returns the URI of the markup template.
* @return the URI of the markup template
* @throws TemplateResourceNotFoundException
* If the template cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected String getSourcePath() {
String template = "/" + this.getClass().getName().replace('.', '/') + ".html";
Resources resource = this.getClass().getAnnotation(Resources.class);
if (StringUtils.isNotEmpty(resource.template())) {
template = resource.template().trim();
if (!template.startsWith("/"))
template = "/" + template;
}
return template;
}
/**
* Returns the URI of the markup template.
* @return the URI of the markup template
* @throws TemplateResourceNotFoundException
* If the template cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected URI getSource()
throws Exception {
return this.getResource(this.getSourcePath());
}
/**
* Returns the {@link InputStream} to the markup template.
* @return the {@link InputStream} to the markup template
* @throws TemplateResourceNotFoundException
* If the source cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected InputStream getSourceStream()
throws Exception {
return this.getResourceStream(this.getSourcePath());
}
/**
* Returns the URI of a resource in the ClassPath.
* @param resource
* @return the URI of a resource in the ClassPath
* @throws TemplateResourceNotFoundException
* If the resource cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected URI getResource(String resource)
throws Exception {
if (StringUtils.isEmpty(resource))
throw new TemplateResourceNotFoundException();
if (Service.class.getResource(resource) == null)
throw new TemplateResourceNotFoundException(resource);
return Service.class.getResource(resource).toURI();
}
/**
* Returns the {@link InputStream} for a resource in the ClassPath.
* @param resource
* @return the {@link InputStream} for a resource in the ClassPath
* @throws TemplateResourceNotFoundException
* If the resource cannot be found in the ClassPath.
* @throws Exception
* In case of unexpected errors.
*/
protected InputStream getResourceStream(String resource)
throws Exception {
if (StringUtils.isEmpty(resource))
throw new TemplateResourceNotFoundException();
if (Service.class.getResource(resource) == null)
throw new TemplateResourceNotFoundException(resource);
return Service.class.getResourceAsStream(resource);
}
/**
* Returns the markup of the template.
* @return the markup of the template
* @throws Exception
* In case of unexpected errors.
*/
protected String getMarkup()
throws Exception {
return new String(IOUtils.toByteArray(this.getSourceStream()));
}
/**
* Returns the data for one data record as preview.
* @return the data for one data record as preview
* @throws Exception
* In case of unexpected errors.
*/
protected abstract Map getPreviewData()
throws Exception;
/**
* Creates the PDF as preview.
* @return the PDF as preview
* @throws Exception
* In case of unexpected errors.
*/
@SuppressWarnings("unchecked")
protected byte[] getPreview()
throws Exception {
return this.generate(new Meta() {{
this.setLocale(Locale.getDefault());
this.setHeader(Template.this.getPreviewData().entrySet().stream()
.filter(e -> !e.getKey().equalsIgnoreCase("static")
&& !(e.getValue() instanceof Collection))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
this.setData(Template.this.getPreviewData().entrySet().stream()
.filter(e -> !e.getKey().equalsIgnoreCase("static"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
this.setFooter(Template.this.getPreviewData().entrySet().stream()
.filter(e -> !e.getKey().equalsIgnoreCase("static")
&& !(e.getValue() instanceof Collection))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
this.setStatics(new HashMap() {
private static final long serialVersionUID = 1L; {
if (Template.this.getPreviewData().containsKey("static"))
this.putAll((HashMap)Template.this.getPreviewData().get("static"));
}});
}});
}
/**
* Creates HTML markup for the PDF based of data in a meta object.
* @param markup
* @param type
* @param meta
* @return the created HTML markup for the PDF creation
*/
protected abstract String generate(String markup, Meta.Type type, Meta meta);
/**
* Merges a collection of PDDocuments into one.
* @param documents
* @return the merged PDDocuments
* @throws IOException
*/
private static PDDocument merge(Collection documents)
throws IOException {
PDFMergerUtility merge = new PDFMergerUtility();
for (PDDocument document : documents) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
merge.addSource(new ByteArrayInputStream(output.toByteArray()));
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
merge.setDestinationStream(output);
merge.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
return PDDocument.load(output.toByteArray());
}
/**
* Normalizes a path.
* Relative directives like . and .. are balanced out.
* If necessary, backslashes are uniformly converted to slashes.
* The return value contains at least one slash.
* @param path
* @return the normalizes a path
*/
private static String normalizePath(String path) {
String string;
String stream;
int cursor;
//path is changed to slash
string = path.replace('\\', '/').trim();
//multiple slashes are combined
while ((cursor = string.indexOf("//")) >= 0)
string = string.substring(0, cursor).concat(string.substring(cursor +1));
//path is balanced if necessary /abc/./def/../ghi -> /abc/ghi
//path is balanced by /.
if (string.endsWith("/."))
string = string.concat("/");
while ((cursor = string.indexOf("/./")) >= 0)
string = string.substring(0, cursor).concat(string.substring(cursor +2));
//path is balanced by /..
if (string.endsWith("/.."))
string = string.concat("/");
while ((cursor = string.indexOf("/../")) >= 0) {
stream = string.substring(cursor +3);
string = string.substring(0, cursor);
cursor = string.lastIndexOf("/");
cursor = Math.max(0, cursor);
string = string.substring(0, cursor).concat(stream);
}
//multiple slashes are combined
while ((cursor = string.indexOf("//")) >= 0)
string = string.substring(0, cursor).concat(string.substring(cursor +1));
return string;
}
/**
* Resolves meta directives #include in markup recursively.
* @param path
* @param markup
* @param stack
* @return the markup with resolved includes
* @throws Exception
* In case of unexpected errors.
*/
private String resolveInlcudes(String path, String markup, List stack)
throws Exception {
Pattern pattern = Pattern.compile("(?i)(?:^|(?<=[\r\n]))\\s*#include(?:(?:\\s+([^\r\n]*)\\s*((?=[\r\n])|$))|(?=\\s*$))");
Matcher matcher = pattern.matcher(markup);
while (matcher.find()) {
if (matcher.groupCount() < 1)
throw new TemplateException("Invalid include found");
String patch = matcher.group(1);
patch = this.followInlcudes(path, patch, stack);
markup = markup.replace(matcher.group(0), patch);
}
return markup;
}
/**
* Follows includes in markup recursively.
* @param path
* @param include
* @param stack
* @return the markup with resolved includes
* @throws Exception
* In case of unexpected errors.
*/
private String followInlcudes(String path, String include, List stack)
throws Exception {
if (include.startsWith("/")
|| include.startsWith("\\"))
include = Template.normalizePath(include);
else include = Template.normalizePath("/" + path + "/" + include);
if (stack.contains(include))
throw new TemplateRecursionException();
List recursions = new ArrayList<>(stack);
recursions.add(include);
if (this.getResource(include) == null)
throw new TemplateResourceNotFoundException(include);
String markup = new String(IOUtils.toByteArray(this.getResourceStream(include)));
try {return this.resolveInlcudes(Template.normalizePath(include + "/.."), markup, recursions);
} catch (TemplateRecursionException exception) {
throw new TemplateException("Recursion found in: " + this.getResource(include));
}
}
/** Exception for endless recursions. */
private static class TemplateRecursionException extends TemplateException {
private static final long serialVersionUID = 6981096067851899978L;
private TemplateRecursionException() {
super();
}
}
/** Exception when accessing and using template resources. */
static class TemplateResourceException extends TemplateException {
private static final long serialVersionUID = -6452881833015318785L;
TemplateResourceException() {
super();
}
TemplateResourceException(String message) {
super(message);
}
}
/** Exception if template resources are not found. */
static class TemplateResourceNotFoundException extends TemplateResourceException {
private static final long serialVersionUID = -4532058335049427299L;
TemplateResourceNotFoundException() {
super();
}
TemplateResourceNotFoundException(String message) {
super(message);
}
}
/**
* Creates the PDF based on the data records as meta object.
* @param meta data records as map array
* @return the created PDF as byte array
* @throws Exception
* In case of unexpected errors.
*/
final protected byte[] generate(Meta meta)
throws Exception {
if (meta == null)
meta = new Meta();
if (meta.data == null)
meta.data = new HashMap<>();
//Header and footer are copied on the first level because both are
//manipulated for the additional values 'page' and 'pages'.
if (meta.header == null)
meta.header = new HashMap<>();
else meta.header = new HashMap<>(meta.header);
if (meta.footer == null)
meta.footer = new HashMap<>();
else meta.footer = new HashMap<>(meta.footer);
URI base = this.getBase();
if (base.getScheme() == null) {
if (Service.class.getResource(base.toString()) == null)
throw new TemplateResourceNotFoundException(base.toString());
base = Service.class.getResource(base.toString()).toURI();
}
if (!base.toString().endsWith("/"))
base = new URI(base.toString() + "/");
String markup = this.getMarkup();
markup = this.resolveInlcudes(this.getBasePath(), markup, new ArrayList<>());
Multiplex multiplex = Multiplex.demux(markup);
List artifacts = new ArrayList<>();
PdfRendererBuilder builder;
ByteArrayOutputStream content = new ByteArrayOutputStream();
builder = new PdfRendererBuilder();
builder.withHtmlContent(this.generate(multiplex.content, Meta.Type.DATA, meta), base.toString());
builder.toStream(content);
builder.run();
artifacts.add(PDDocument.load(content.toByteArray()));
try (PDDocument document = Template.merge(artifacts)) {
Splitter splitter = new Splitter();
List pages = splitter.split(document);
meta.header.put("pages", String.valueOf(pages.size()));
meta.footer.put("pages", String.valueOf(pages.size()));
for (PDDocument page : new ArrayList<>(pages)) {
int offset = pages.indexOf(page);
meta.header.put("page", String.valueOf(offset +1));
ByteArrayOutputStream header = new ByteArrayOutputStream();
builder = new PdfRendererBuilder();
builder.withHtmlContent(this.generate(multiplex.header, Meta.Type.HEADER, meta), base.toString());
builder.toStream(header);
builder.run();
try (Overlay overlay = new Overlay()) {
overlay.setInputPDF(page);
overlay.setAllPagesOverlayPDF(PDDocument.load(header.toByteArray()));
ByteArrayOutputStream output = new ByteArrayOutputStream();
overlay.overlay(new HashMap<>()).save(output);
page = PDDocument.load(output.toByteArray());
}
meta.footer.put("page", String.valueOf(offset +1));
ByteArrayOutputStream footer = new ByteArrayOutputStream();
builder = new PdfRendererBuilder();
builder.withHtmlContent(this.generate(multiplex.footer, Meta.Type.FOOTER, meta), base.toString());
builder.toStream(footer);
builder.run();
try (Overlay overlay = new Overlay()) {
overlay.setInputPDF(page);
overlay.setAllPagesOverlayPDF(PDDocument.load(footer.toByteArray()));
ByteArrayOutputStream output = new ByteArrayOutputStream();
overlay.overlay(new HashMap<>()).save(output);
page = PDDocument.load(output.toByteArray());
}
pages.add(offset +1, page);
pages.remove(offset);
}
try (PDDocument release = Template.merge(pages)) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
release.save(output);
return output.toByteArray();
}
}
}
@Override
public String toString() {
return this.getSourcePath();
}
/**
* The template can contain three fragments: header, content, footer.
* The content can be generated in one step. Because the content is
* framed in a page and the page can contain margins in which headers
* and footers must be inserted. Header and footer must therefore be
* inserted later as an overlay. Header and footer can contain their
* own placeholders and are therefore generated separately. So the
* special placeholders page (current page number) and pages (total
* number) can also be used.
*
* The multiplexer separates the markup for the fragments: header,
* content, footer. So that three templates can be created from one
* template and each fragment can be rendered individually as a PDF.
*/
private static class Multiplex {
/** markup of header */
private String header;
/** markup of content */
private String content;
/** markup of footer */
private String footer;
/**
* Creates a copy of the passed document.
* @param document
* @return a copy of the passed document
* @throws ParserConfigurationException
*/
private static Document documentClone(Document document)
throws ParserConfigurationException {
Node root = document.getDocumentElement();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document clone = builder.newDocument();
clone.appendChild(clone.importNode(root, true));
return clone;
}
/**
* Removes elements from the document which match the selector.
* @param document
* @param selector
* @throws XPathExpressionException
*/
private static void documentRemoveNode(Document document, String selector)
throws XPathExpressionException {
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression expression = xpath.compile(selector);
NodeList nodes = (NodeList)expression.evaluate(document, XPathConstants.NODESET);
for (int loop = 0; loop < nodes.getLength(); loop++)
nodes.item(loop).getParentNode().removeChild(nodes.item(loop));
}
/**
* Adds a borderless style to the document.
* This style is needed for the header and footer.
* @param document
* @throws XPathExpressionException
*/
private static void documentBorderless(Document document)
throws XPathExpressionException {
Element style = document.createElement("style");
style.setAttribute("type", "text/css");
style.setTextContent("@page {"
+ " margin:0mm!important;"
+ " padding:0mm!important;"
+ " border:0mm solid!important;}");
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression expression = xpath.compile("/html/head");
Node node = (Node)expression.evaluate(document, XPathConstants.NODE);
if (node != null)
node.appendChild(style);
}
/**
* Creates an XML string based on the document.
* @param document
* @return an XML string based on the document
* @throws TransformerException
*/
private static String documentToString(Document document)
throws TransformerException {
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
transformer.transform(new DOMSource(document), new StreamResult(buffer));
return buffer.toString();
}
/**
* Separates the markup for the fragments: header, content, footer.
* @param markup
* @return a multiplex instance with the extracted markup for
* header, content an footer
* @throws Exception
*/
private static Multiplex demux(String markup)
throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new ByteArrayInputStream(markup.getBytes()));
Multiplex multiplex = new Multiplex();
Document content = Multiplex.documentClone(document);
Multiplex.documentRemoveNode(content, "/html/body/header");
Multiplex.documentRemoveNode(content, "/html/body/footer");
multiplex.content = Multiplex.documentToString(content);
Document header = Multiplex.documentClone(document);
Multiplex.documentRemoveNode(header, "/html/body/*[name() != 'header']");
Multiplex.documentBorderless(header);
multiplex.header = Multiplex.documentToString(header);
Document footer = Multiplex.documentClone(document);
Multiplex.documentRemoveNode(footer, "/html/body/*[name() != 'footer']");
Multiplex.documentBorderless(footer);
multiplex.footer = Multiplex.documentToString(footer);
return multiplex;
}
}
/** TemplateException */
public static class TemplateException extends ServiceException {
private static final long serialVersionUID = 2701029837610928746L;
/** TemplateException */
TemplateException() {
super();
}
/**
* TemplateException
* @param cause
*/
TemplateException(Throwable cause) {
super(cause);
}
/**
* TemplateException
* @param message
*/
TemplateException(String message) {
super(message);
}
/**
* TemplateException
* @param message
* @param cause
*/
TemplateException(String message, Throwable cause) {
super(message, cause);
}
}
}
/** ServiceException */
public static class ServiceException extends Exception {
private static final long serialVersionUID = -6124067136782291330L;
/** ServiceException */
ServiceException() {
super();
}
/**
* ServiceException
* @param cause
*/
ServiceException(Throwable cause) {
super(cause);
}
/**
* ServiceException
* @param message
*/
ServiceException(String message) {
super(message);
}
/**
* ServiceException
* @param message
* @param cause
*/
ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy