sirius.web.templates.Templates Maven / Gradle / Ivy
Show all versions of sirius-web Show documentation
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.web.templates;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import sirius.kernel.Sirius;
import sirius.kernel.async.CallContext;
import sirius.kernel.commons.Context;
import sirius.kernel.commons.Strings;
import sirius.kernel.di.GlobalContext;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Parts;
import sirius.kernel.di.std.PriorityParts;
import sirius.kernel.di.std.Register;
import sirius.kernel.extensions.Extension;
import sirius.kernel.extensions.Extensions;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.HandledException;
import sirius.kernel.health.Log;
import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Collection;
import java.util.List;
/**
* Content generator which generates output based on templates.
*
* In contrast to the web server and its handlers Velocity is used here as template engine. This is because
* these templates are easier to write as they don't need andy type information. as these templates are less
* frequently executed the lower performance does not matter. The language reference of velocity, which is one of the
* most commonly used language for templates can be found here:
* http://velocity.apache.org/engine/devel/vtl-reference-guide.html
*
* The template sources are loaded via {@link Resources#resolve(String)}. If no resolver is available or none of the
* available ones can load the template, it is tried to load the template from the classpath.
*
* To extend the built in velocity engine, macro libraries can be enumerated in the system config under
* content.velocity-libraries (For examples see component-web.conf). Also {@link ContentContextExtender} can
* be implemented and registered in order to provider default variables within the execution context.
*
* Specific output types are generated by {@link ContentHandler} implementations. Those are either picked by the file
* name of the template, or by setting {@link Generator#handler(String)}. So if a file ends with .pdf.vm it is
* first evaluated by velocity (expecting to generate XHTML) and then rendered to a PDF by flying saucer.
* Alternatively the handler type pdf-vm can be set to ensure that this handler is picked.
*/
@Register(classes = Templates.class)
public class Templates {
/**
* If a specific output encoding is required (other than the system encoding - most definitely UTF-8) a variable
* using this key can be supplied to the generator, specifying the name of the encoding to use.
*
* If possible however it is preferable to use {@link Generator#encoding(String)} to set the encoding.
*/
public static final String ENCODING = "encoding";
/*
* Logger used by the content generator framework
*/
public static final Log LOG = Log.get("templates");
/*
* Contains all implementations of ContentHandler sorted by getPriority ascending
*/
@PriorityParts(ContentHandler.class)
private Collection handlers;
/*
* Contains all implementations of ContentContextExtender
*/
@Parts(ContentContextExtender.class)
private Collection extenders;
@sirius.kernel.di.std.Context
private GlobalContext ctx;
@Part
private Resources resources;
/**
* Used to generate content by either evaluating a template or directly supplied template code.
*
* This uses a builder like pattern (a.k.a. fluent API) and requires to either call {@link #generate()} or
* {@link #generateTo(OutputStream)} to finally generate the content.
*/
public class Generator {
private String templateName;
private String templateCode;
private String handlerType;
private Context context = Context.create();
private String encoding;
/**
* Applies the context to the generator.
*
* This will join the given context with the one previously set (Or the system context). All values with
* the same name will be overwritten using the values in the given context.
*
* @param ctx the context to be applied to the one already present
* @return the generator itself for fluent API calls
*/
public Generator applyContext(Context ctx) {
context.putAll(ctx);
return this;
}
/**
* Adds a variable with the given name (key) and value to the internal context.
*
* If a value with the same key was already defined, it will be overwritten.
*
* @param key the name of the variable to set
* @param value the value of the variable to set
* @return the generator itself for fluent API calls
*/
public Generator put(String key, Object value) {
context.put(key, value);
return this;
}
/**
* Sets the output encoding which is used to generate the output files.
*
* @param encoding the encoding to use for output files
* @return the generator itself for fluent API calls
*/
public Generator encoding(String encoding) {
this.encoding = encoding;
return this;
}
/**
* Determines which template file should be used.
*
* The content is resolved by calling {@link Resources#resolve(String)}.
*
* @param templateName the name of the template to use
* @return the generator itself for fluent API calls
*/
public Generator useTemplate(String templateName) {
this.templateName = templateName;
return this;
}
/**
* Sets the template code to be used directly as string.
*
* Most probably this will be velocity code. Once a direct code is set, the template specified by
* {@link #useTemplate(String)} will be ignored.
*
* @param templateCode the template code to evaluate
* @param handlerType String reference for the handler to be used
* (i.e. {@link sirius.web.templates.velocity.VelocityContentHandler#VM})
* @return the generator itself for fluent API calls
*/
public Generator direct(String templateCode, String handlerType) {
this.templateCode = templateCode;
handler(handlerType);
return this;
}
/**
* Specifies which {@link ContentHandler} is used to generate the content.
*
* Most of the time, the content handler is auto-detected using the file name of the template. An example
* would be .pdf.vm which will force the {@link sirius.web.templates.velocity.VelocityPDFContentHandler}
* to generate a PDF file using the template. However, by using {@code generator.handler("pdf-vm")}
* it can be ensured, that this handler is picked, without relying on the file name.
*
* @param handlerType the name of the handler type to use. Constants can be found by looking at the
* {@link Register} annotations of the implementing classes of {@link ContentHandler}.
* @return the generator itself for fluent API calls
*/
public Generator handler(String handlerType) {
this.handlerType = handlerType;
return this;
}
/**
* Calls the appropriate {@link ContentHandler} to generate the output which is written into the given
* output stream.
*
* @param out the output stream to which the generated content is written
*/
public void generateTo(OutputStream out) {
if (Strings.isFilled(templateName)) {
CallContext.getCurrent().addToMDC("content-generator-template", templateName);
}
try {
try {
if (Strings.isFilled(handlerType)) {
generateContentUsingHandler(out);
} else {
findAndInvokeContentHandler(out);
}
} catch (HandledException e) {
throw e;
} catch (Throwable e) {
throw Exceptions.handle()
.error(e)
.to(LOG)
.withSystemErrorMessage("Error applying template '%s': %s (%s)",
Strings.isEmpty(templateName) ? templateCode : templateName)
.handle();
}
} finally {
CallContext.getCurrent().removeFromMDC("content-generator-template");
}
}
private void findAndInvokeContentHandler(OutputStream out) throws Exception {
for (ContentHandler handler : handlers) {
if (handler.generate(this, out)) {
return;
}
}
throw Exceptions.handle()
.to(LOG)
.withSystemErrorMessage("No handler was able to render the given template: %s",
Strings.isEmpty(templateName) ? templateCode : templateName)
.handle();
}
private void generateContentUsingHandler(OutputStream out) throws Exception {
ContentHandler handler = ctx.findPart(handlerType, ContentHandler.class);
if (!handler.generate(this, out)) {
throw Exceptions.handle()
.to(LOG)
.withSystemErrorMessage("Error using '%s' to generate template '%s'.",
handlerType,
Strings.isEmpty(templateName) ? templateCode : templateName)
.handle();
}
}
/**
* Invokes the appropriate {@link ContentHandler} and returns the generated content handler as string.
*
* Most probably the input will be a velocity template which generates readable text (which also might be
* XML or HTML).
*
* @return the generated string contents
*/
public String generate() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
generateTo(out);
return new String(out.toByteArray(), Charsets.UTF_8);
}
/**
* Can be used by a {@link ContentHandler} to obtain a preset templateName.
*
* @return the templateName which was previously set or null if no template name was given
*/
public String getTemplateName() {
return templateName;
}
/**
* Can be used by a {@link ContentHandler} to obtain a preset templateCode.
*
* @return the templateCode which was previously set or null if no template code was given
*/
public String getTemplateCode() {
return templateCode;
}
/**
* Can be used by a {@link ContentHandler} to access the context which contains all previously set variables.
*
* @return the previously set context containing all applied variables.
*/
public Context getContext() {
return context;
}
/**
* Can be used by a {@link ContentHandler} to determine the file ending of the selected template. This is
* used to select which content handler is actually used to generate the output.
*
* @param extension the expected file extension, without a "." at the beginning
* @return true if the given template ends with the given extension, false otherwise. This
* first dot is considered the start of the file extension so "foobar.test.js" has "test.js" as extension.
* If the templateName is null, this method always returns false.
*/
public boolean isTemplateFileExtension(@Nonnull String extension) {
return Strings.isFilled(templateName) && extension.equalsIgnoreCase(Strings.split(templateName, ".")
.getSecond());
}
/**
* Can be used by a {@link ContentHandler} to determine the effective ending of the underlying template name.
*
* @param extension the expected end of the file name.
* @return true if the given template ends with the given extension, false otherwise.
* In contrast to {@link #isTemplateFileExtension(String)} this will not consider the first "." to be the
* file extension but rather really check if the template name ends with the given extension. Therefore
* for a template named test.js.vm this will return true for
* {@code isTemplateEndsWith(".vm")} but false for {@code isTemplateFileExtension("vm")}
*/
public boolean isTemplateEndsWith(@Nonnull String extension) {
return Strings.isFilled(templateName) && extension.toLowerCase().endsWith(extension);
}
/**
* Can be used by a {@link ContentHandler} to determine the effective encoding used for the generated output.
* This is either set via {@link #encoding(String)} or by placing a variable named {@link #ENCODING} in the
* context or it is the default encoding used by the JVM (most probably UTF-8).
*
* @return the effective encoding used to generate the output
*/
public String getEncoding() {
if (Strings.isFilled(encoding)) {
return encoding;
}
if (context.containsKey(ENCODING)) {
return (String) context.get(ENCODING);
}
return Charsets.UTF_8.name();
}
/**
* Contains the handler type. This can be used by a {@link ContentHandler} to skip all filename checks and
* always generate its output.
*
* @return the handlerType previously set using {@link #handler(String)} or null if no handler type
* was set.
*/
public String getHandlerType() {
return handlerType;
}
/**
* Uses the {@link Resolver} implementations or the classloader to load the template as input stream.
*
* @return the contents of the template as stream or null if the template cannot be resolved
*/
public InputStream getTemplate() {
try {
if (templateName == null) {
throw Exceptions.handle()
.to(LOG)
.withSystemErrorMessage("No template was given to evaluate.")
.handle();
}
URL url = resources.resolve(templateName).map(r -> r.getUrl()).orElse(null);
if (url == null) {
throw Exceptions.handle()
.to(LOG)
.withSystemErrorMessage("Unable to resolve '%s'", templateName)
.handle();
}
return url.openStream();
} catch (IOException e) {
throw Exceptions.handle(LOG, e);
}
}
}
/**
* Creates a new generator which can be used to generate a template based output.
*
* @return a new {@link Generator} which can be used to generate output
*/
public Generator generator() {
Generator result = new Generator();
for (ContentContextExtender extender : extenders) {
extender.extend(result.getContext());
}
return result;
}
/**
* Returns a list of all extensions provided for the given key.
*
* This can be used to provide templates that contain sections which can be extended by other
* components. Think of a generic template containing a menu. Items can be added to this menu
* using this mechanism.
*
* Internally the {@link Extensions} framework is used. Therefore all extensions
* for the key X have to be defined in content.extensions.X like this:
*
* content.extensions {
* X {
* extension-a {
* priority = 110
* template = "a.html"
* }
* extension-b {
* priority = 120
* template = "b.html"
* }
* }
* }
*
*
* To utilize these extensions in Rythm, use the includeExtensions("name") tag. For Velocity a macro with
* the same name is provided.
*
* @param key the name of the list of content extensions to retrieve
* @return a list of templates registered for the given extension using the system config and the Extensions
* framework
* @see Extensions
*/
public List getExtensions(String key) {
List result = Lists.newArrayList();
for (Extension e : Extensions.getExtensions("content.extensions." + key)) {
if (Sirius.isFrameworkEnabled(e.get("framework").asString())) {
result.add(e.get("template").asString());
}
}
return result;
}
}