Juzu is a web framework based on MVC concepts for developing applications. Juzu is an open source project developed on GitHub licensed under the Apache 2 License.
This chapter continues the exploration of the architecture of Juzu, it is an essential read if you want to understand how Juzu globally works and a recommanded read if you extend Juzu. Juzu philosophy is to provide a minimal core that is extended with plugins: this design allows to extend Juzu with no need to touch its core.
Plugins
Juzu plugins can be classified according several criterions. Let’s study the different kind of plugins possible.
Metamodel plugins
A metamodel plugin is executed at compilation time when the application is compiled. Usually it reacts to Java annotations in order to:
-
Validate the application : make checks depending on Java annotations
-
Read or write application resources : create new resources
-
Generate Java code : the generated code will be transparently compiled by the Java compiler
When a metamodel plugin is not happy, it can fail the current by reporting an error to the Java compiler.
Runtime plugins
A runtime plugin is executed during the execution of the web application. A runtime plugin can create objects that will be available for the application. Such objects are usually consumed by the application thanks to the IOC container.
Provider factories
A provider factory provides plugability for integrating beans that are not managed natively by the IOC container but needs to be integrated inside the container.
Templating engine
Juzu can easily be extended with new templating engine. Templating engine integrates of course at runtime, however they can optionally be part of the compilation phase of the application for validating templates or for creating resources or classes to be used at runtime.
Value types
Value types is a Juzu plugin for dealing with controller parameters that can trivially be converted from and to strings.
In this section we will explain how the juzu.impl.value.ValueType
interface works.
Implementing the ValueType
public abstract class ValueType<T> {
/**
* The list of java classes this implementation can handle.
*
* @return the list of types.
*/
public abstract Iterable<Class<?>> getTypes();
/**
* Parse a string and returns the corresponding value type.
*
* @param element the element annotations
* @param s the string to parse
* @return the corresponding value
* @throws java.lang.Exception any exception preventing the parse to succeed
*/
public abstract T parse(AnnotatedElement element, String s) throws Exception;
/**
* Format a value and returns the corresponding string.
*
* @param element the element annotations
* @param value the value to format
* @return the corresponding string
*/
public abstract String format(AnnotatedElement element, T value);
}
The base implementation should extend the ValueType
interface and provide a class type for the <T>
generic parameter
of the class:
public class DateValueType extends ValueType<java.util.Date> {
...
}
The getTypes()
interface should return the class type the value type is converting:
public Iterable<Class<?>> getTypes() {
return Collections.singleton(java.util.Date.class);
}
several types can be returned by the method, this is used by the ValueType that handle primitive types such
as int , boolean , etc…
|
Finally the parse
and format
methods need to be implemented too:
public Date parse(AnnotatedElement element, String s) throws ParseException {
return getSimpleDateFormat(element).parse(s);
}
public String format(AnnotatedElement element, Date value) {
return getSimpleDateFormat(element).format(value);
}
The parse
and format
methods provides access to the element
that is converted, the purpose of those
objects is to provide access to optional annotation the element may have. Such annotations can be used by the
value type for doing the conversion:
private SimpleDateFormat getSimpleDateFormat(AnnotatedElement element) {
Format format = element.getAnnotation(Format.class);
return format != null ? new SimpleDateFormat(format.value()) : new SimpleDateFormat();
}
Declaring the ValueType
Value types are declared in the META-INF/services/juzu.impl.value.ValueType
file, such plugins are
loaded via the java.util.ServiceLoader
interface.
Entity processing
Juzu allows the application to interact with request and response entities with the juzu.request.ClientContext
and
the juzu.Response.Body
objects. Those objects are binary data (or character data) oriented and provide a low
level interface for reading or writing request entities.
Juzu provides also a plugable higher level API for dealing with request entities such as File Upload or Json. In this section we will explain how to implement such plugins.
Entity unmarshallers
Entity unmarshallers are plugins that decodes a juzu.request.ClientContext
into a set of arguments that will be
used by Juzu when invoking the controller. The unmarshaller can manipulate those arguments, usually reading the
request entity. The juzu.impl.request.EntityUnmarshaller
abstract class has two methods to implement, let’s review
them.
public abstract class EntityUnmarshaller {
/**
* Decide wether or not this reader accept to read the specified <code>mediaType</code> argument.
*
* @param mediaType the media type to test
* @return true if the media type is accepted
*/
public abstract boolean accept(String mediaType);
/**
* Unmarshall the entity from the {@link juzu.request.ClientContext}. This unmarshaller can modify the
* request control arguments:
* <ul>
* <li>the contextual arguments can be iterated and the value updated</li>
* <li>the parameter arguments map can be freely modified</li>
* </ul>
*
* @param mediaType the request media type
* @param context the client context for reading the entity
* @param contextualArguments the contextual arguments
* @param parameterArguments the contextual parameters
* @throws IOException anything preventing the read operation to succeed
*/
public abstract void unmarshall(
String mediaType,
ClientContext context,
Iterable<Map.Entry<ContextualParameter, Object>> contextualArguments,
Map<String, RequestParameter> parameterArguments) throws IOException;
}
Implementing an unmarshaller
The accept
method is quite simple to implement, it should return true when the unmarshaller wants to unmarshall
the incoming request, for instance:
@Override
public boolean accept(String mediaType) {
return mediaType.equals("application/json");
}
The unmarshall
method will be invoked when the accept
method returns true. At this moment, the unmarshaller should
read the entity data using the ClientContext
interface and update the request arguments. There are two kinds of
request arguments that can be modified:
-
parameter arguments using the
parameterArguments
method parameter -
contextual arguments using the
contextualArguments
method parameter
Parameter arguments is a mutable map that provides string parameter values, those parameters are used by Juzu at
a later stage when invoking the controller handler. Those parameters can be used as is, or can be transformed into
other types via the ValueType
extension or the @Mapped
bean mapping. The File Upload unmarshaller
will create parameter arguments when reading the file upload form.
Contextual arguments are any controller argument that are not parameter arguments (i.e any type that is not a ValueType
or annotated with @Mapped
). The unmarshall method can iterate over such arguments and modify them freely. The
ContextualParameter
object provides the name, class+ and _generic type+ of the argument. Those shall be used by
the unmarshaller to properly modify the arguments. The _File Upload unmarshaller will look at the contextual
arguments having the type org.apache.commons.fileupload.FileItem
and the right argument name to do the match.
Entity marshallers
Entity marshallers are plugins that encodes an object, usually returned by a controller handler into binary data
sent with the response. The juzu.impl.request.EntityMarshaller
abstract class has a single method marshall
to implement, let’s review it.
Implementing a marshaller
public abstract class EntityMarshaller {
/**
* Marshall the object for the specified <code>mimeType</code> or return null.
*
* @param mimeType the mime type to test
* @param annotations the contextual annotations
* @param object the object to marshall @return the corresponding streamable
*/
public abstract Streamable marshall(String mimeType, AnnotatedElement annotations, Object object);
}
The marshall method will be invoked by Juzu, when a controller returns an unknown object response (i.e an object
that does not inherit from the juzu.Response
class). The marshall method can either return a null response to
signal that it cannot handle it or a Streamable
implementation that will be used to create a juzu.Response
.
Asset minifier
An asset minifier is simple interface providing the opportunity to transform the original source files into a new version that is a minified version of this source.
A minifier should implement the juzu.plugin.assetMinifier
interface and provide a no argument constructor.
Minification occurs at compilation time when the annotations are processed: Juzu instantiates a minifier to
transform the physical asset and write the minified version on the disk. The minified asset name uses the original
asset name with the -min suffix placed before the asset name extension, for instance jquery.js will be
named as jquery-min.js, this is totally transparent for the user.
Let’s look at the implementation of the minifier:
public class NormalizeJSMinifier implements Minifier {
@Override
public InputStream minify(String name, String type, InputStream stream) throws IOException {
if (type.equals("script")) {
NormalizeJSReader reader = new NormalizeJSReader(new InputStreamReader(stream));
String s = Tools.read(reader);
return new ByteArrayInputStream(s.getBytes());
} else {
throw new IOException("Can only process scripts and not " + type + " asset");
}
}
}
The minify
method provides three arguments:
. the asset value, for instance jquery.js
. the asset type with possible values of script and stylesheet
. the asset stream
The implementation is quite simple it wraps, the InputStream
argument and returns the minifying stream.
in this implementation the stream is buffered before returning the stream because Java does not provide an
out of the box ReaderInputStream but if it would be the case, that could be done on the fly instead of buffering. In
all case the implementation can chose between buffering the stream or transforming on the fly.
|
Provider factories
Provider factories provide plugability for integrating beans that are not managed by the IOC container. A provider
factory is dynamic: it is a factory for javax.inject.Provider
that returns a provider for a specificied class. Usually
provider factories will lookup the service in a registry (like another IOC container) and returns a provider that
return them lazily or not.
The provider factory defines the getProvider
method:
/**
* Returns a provider for a specific type or null if it cannot be produced.
*
* @param implementationType the implementation class object
* @param <T> the implementation generic type
* @return a provider for this class or null
* @throws Exception any exception that would prevent to obtain the provider
*/
<T> Provider<? extends T> getProvider(Class<T> implementationType)
throws Exception;
The factory implementation must provide a public zero argument constructor and it will be instantiated during the application boostrap by Juzu to obtain the provider. The returned providers will then be bound into the IOC container.
The IOC container uses the java.util.ServiceLoader
discovery mechanism for finding provider factories when
injection occurs.
Let’s study a simple example with a provider for the current time:
package my;
public class TimeProviderFactory implements java.inject.ProviderFactory {
public <T> Provider<? extends T> getProvider(final Class<T> implementationType) throws Exception {
if (implementationType == java.util.Date.class) {
return new Provider<T>() {
public T get() {
return implementationType.cast(new java.util.Date());
}
};
}
else {
return null;
}
}
}
This provider should be declared in the META-INF/services/juzu.inject.ProviderFactory file:
my.TimeProvider
Templating SPI
This chapter dives into the template life cycle from the compilation time to the run time. We will describe the template Service Provider Interface (SPI), the SPI is designed to make Juzu templating extensible and integrating template engines in Juzu. This chapter is optional is you are only writing ab application with Juzu, however it is a must read if you want to know more Juzu internals or if you want to understand how to integrate a template engine in Juzu.
When a Juzu application is compiled, the Juzu annotation processor detects the @Path
annotations and triggers
the compilation of the related templates. The template compilation can be split in two parts:
-
Generating the template companion class that inherits the
juzu.template.Template
class. This part is generic and works with any templating system, it is entirely managed by Juzu. -
Processing the template file, this task is delegated to the
TemplateProvider
interface and is extensible. The provider allows to have several templating system in Juzu and decouples the template compilation process from the details of the templating engine.
Compiling a Groovy template
Let’s study an example with the Groovy template at compilation time.
When the Java compiler is invoked, the following steps are executed
-
The Java compiler triggers the Juzu annotation processor when it finds the
@Path
annotation -
Juzu resolves the relative path to the
templates
package of the application-
When the template cannot be resolved a compilation error is triggered
-
Otherwise the template is loaded
-
-
The template provider is looked up according to the file name extension, it generates the index.groovy_ source file
-
Juzu creates the
index
class that extends thejuzu.template.Template
class annotated by the@Path("index.gtmpl")
annotation
After that the only remaining part is to compile the index.groovy_ source to a class. It can be achieved either at build time
using the groovyc compiler or at load time when the index
template is loaded using a GroovyClassLoader
. The former
approach makes the build a bit more complex (but not much as Groovy compilation is fairly well supported in build systems or IDEs)
as it requires to run a Groovy compilation but it will perform additional validation of the template as well as reduce the load
time of the template. The later approach will detect any compilation error (such as Groovy syntax error) at runtime
and the index.groovy_ compilation will take a few milliseconds.
This flexibility allows to use the lazy approach during development and when the application is released then the Groovy compiler can be used to compile the index.groovy_.
Type safe URL resolution
Groovy templates provides the @{…}
syntax for generating URL from the application controllers. This section gives
an overview of the underlying resolution mechanism.
-
Parse: the template is parsed into its model representation
-
Resolve: the
index
link is resolved againt the controller meta model -
Validate: the
index
link is validated -
Emit: the corresponding index.groovy_ file is emitted and save on the class output
-
Compile: the Groovy source is compiled into a class by the groovyc compiler (this part is done after javac)
Template Service Provider Interface
Juzu provides a Service Provider Interface (SPI) for integrating thirdparty template engine. Actually all template system are integrated with the SPI. We will study briefly the integration points so you can integrate a template engine of your choice in Juzu.
Template providers
The juzu.impl.template.spi.TemplateProvider
is the main entry point when a templating system is integrated. The
provider is triggered during the compilation phase by the APT system built into the Java compiler.
public abstract class TemplateProvider<M extends Serializable> {
...
}
The provider must declare the template model <M>
generic type, this type must be a serializable as Juzu will sometimes
write template models on the disk during the compilation. This usually happens only in Eclipse due its incremental
compiler architecture. The type specified by the provider is privately managed (i.e it is opaque for Juzu) and it
symbolizes an internal representation of the parsed source (usually an Abstract Syntax Tree), it will be used in
various methods of the provider.
Let’s have a review of the methods of this class to have a better understanding.
/**
* Returns the template source extension without the dot recognised by
* this provider. For instance it should return <code>gtmpl</code>
* for groovy templates.
*
* @return the source extension
*/
public abstract String getSourceExtension();
The getSourceExtension()
method is used to determine what file extension the provider can compile. The implementation
should return a constant value, for instance the Groovy provide simply returns the gtmpl
value.
/**
* Parse the provided char sequence and return the corresponding template model.
*
* @param context the parse context
* @param source the source to parse
* @return the corresponding template model
* @throws TemplateException any template related exception
*/
public abstract M parse(
ParseContext context,
CharSequence source) throws TemplateException;
/**
* Process the template.
*
* @param context the process context
* @param templateModel the template to process
* @throws TemplateException any template related exception
*/
public abstract void process(
ProcessContext context,
TemplateModel<M> templateModel) throws TemplateException;
/**
* Provide an opportunity for emitting a file on the disk.
*
* @param context the emit context
* @param templateModel the template
* @throws TemplateException any template related exception
* @throws IOException any io exception
*/
public abstract void emit(
EmitContext context,
TemplateModel<M> templateModel) throws TemplateException, IOException;
The parse
, process
and emit
methods care about transforming the template source to its final representation:
the compiled template.
-
The
parse
method is invoked with the content of the template and returns a template model. The representation returned by the parse method is a parsed representation of the template source. If a parsing error occurs the method can throw aTemplateException
. -
The
process
method is invoked after the template is parsed with the necessary context for performing further processing of the template, for instance the Groovy templating engine performs the resolution of type safe URLs or type safe parameters declaration at this moment. During the process:-
The provider can resolve other templates using the
ProcessContext
, if the template to resolve is not yet loaded it will trigger theparse
/process
/emit
lifecycle, it if was already processed the template is simply returned -
The implementation can resolve controller methods and translate them into method invocation, this is used for checking type safe URL and translating them into controller companion invocation
-
The
juzu.impl.template.spi.TemplateModel
argument models the template, it has several fields such as the underlying model or the template path -
The implementation can declare type safe parameters using the
TemplateModel#addParameter(String)
method. The declared parameters will be generated on thejuzu.template.Template
subclass
-
-
The
emit
method is invoked when the template processing is over. TheEmitContext
interface can be used to create resources during this round.
/**
* Return the template stub type.
*
* @return the template stub class
*/
public abstract Class<? extends TemplateStub> getTemplateStubType();
Finally the getTemplateStubType()
returns the type of a java class that will be used for creating a template stub.
For each template, a stub is created, the stub is responsible for loading the template at runtime, i.e the original
template or the compiled template that may have been generated during compilation during the emit
callback.
Template stub
Template stubs are java classes created by Juzu for managing a template at runtime on behalf of the provider.
Each provider provides its own stub implementation as a juzu.impl.template.spi.TemplateStub
subclass.
A stub must provide a public constructor accepting a java.lang.String
argument: the template id. The template
id if the class name of the generated template. In addition, a stub must implement two abstract methods:
/**
* Init the template with the associated resource.
*
* @param loader the class loader
*/
protected abstract void doInit(ClassLoader loader);
/**
* Performs template rendering.
*
* @param renderContext the render context
* @throws TemplateExecutionException any execution exception
* @throws IOException any io exception
*/
protected abstract void doRender(TemplateRenderContext renderContext)
throws TemplateExecutionException, IOException;
The doInit
method loads the template using the provided ClassLoader
, it will be call only once before the
template is rendered. Usually it uses the template id provided during the construction of the template to
locate the template on the disk, in its original form or in its compiled form.
The doRender
method renders the template using the provided TemplateRenderContext
. The render context
provides the necessary hooks such as:
-
Producing markup
-
Setting the title
-
Obtaining the locale
-
Accessing parameters or application beans for resolving expressions
Template at work
After having described the various pieces of the templating SPI, let’s look at how the template generated stubs are used by Juzu templating system at runtime.
When the controller declares the index.gtmpl template the compiler produces three artifacts
* the index
class template inherits juzu.template.Template
: it is the only class visible from the
controller and the whole application
* the index.groovy_ Groovy template is the effective template code: it produces the markup, resolve expressions, etc…
When a controller is instantiated, the index
template instance is injected into the controller, the @Path
annotation
plays an essential role because it’s a qualifier and that qualifier is used to distinguish the correct subclass to inject
in the controller.
When the template is created, the corresponding template stub is instantiated. When the template needs to be
rendered, the doInit(ClassLoader)
method of the stub is invoked. At this moment the Groovy index_
class is
loaded from the class loader, when the class is not found, the index.groovy_ is loaded and it is compiled
on the fly.
Qualified template class
Controller can be injected with the juzu.template.Template
class, however they can also be injected with
the template subclass that was genereted by Juzu: instead of using the qualified template injection,
the controller declares the template index subclass. This approach cab be used when type safe parameters
are used as only the index
type declares the fluent API.
For instance if the index.gtmpl declares the color parameter the index
class will look like:
@Path("index.gtmpl")
public class index extends Template {
...
public index with() {
return new index.Builder();
}
public class Builder extends Template.Builder {
public Builder color(String color) {
// Generated code
}
}
}
The controller can then use the fluent API:
public class Controller {
@Inject
@Path("index.gtmpl")
Template index;
@View
public Response.Content index() {
return index.with().color("red").ok();
}
}
Bundlegen plugin tutorial
The bundlegen plugin tutorial provides guidance for the implementation of a Juzu metamodel plugin.
Unbreakable resource bundles
The bundlegen plugin generates a typed Java class for a resource bundle:
@BundleGen("mybundle")
@Application
package my.app;
Le’t suppose we have this mybundle.properties
file:
hello_world=Hello World
This will generate a bundlegen
Java class, the application can then retrieve the entries via static methods:
String s = bundlegen.hello_world();
The bundle will be loaded using the current user locale, providing unbreakable resource bundles!
The implementation
We will provide now a detailled analysis of the plugin structure and code. This plugin is a metamodel plugin,
it does all the work during the compilation: it processes @BundleGen
annotations in order to generate
a Java class source for this bundle. The generated source will be transparently compiled by the Java compiler
in the same compilation phase than the application.
The plugin will be packaged under the examples.plugin.bundlegen
package and provides three classes:
-
examples.plugin.bundlegen.BundleGen
annotation is the plugin API : the application will use this annotation for describing its bundles -
examples.plugin.bundlegen.impl.BundlResolver
is an utility class used by the generated code at runtime for loading a bundle -
examples.plugin.bundlegen.impl.BundleGenMetaModelPlugin
is the metamodel plugin triggered during by the compilation phase by Juzu
The metamodel plugin structure
Let’s begin by describing the overral plugin structure. The metamodel plugin BundleGenMetaModelPlugin
extends
the juzu.impl.plugin.application.metamodel.ApplicationMetaModelPlugin
base class. It must provide a no arguments public constructor
public BundleGenMetaModelPlugin() {
super("bundlegen");
}
This plugin must be declared in the META-INF/services/juzu.impl.plugin.application.metamodel.ApplicationMetaModelPlugin file:
examples.plugin.bundlegen.impl.BundleGenMetaModelPlugin
To process the @BundleGen
annotation the plugin needs to declare the annotations it has interest for, this
is done in the init
method.
@Override
public Set<Class<? extends Annotation>> init(ProcessingContext env) {
return Collections.<Class<? extends Annotation>>singleton(BundleGen.class);
}
Processing the @BundleGen
annotation
During compilation our plugin will receive lifecycle callbacks for the BundleGen
annotation:
@Override
public void processAnnotationAdded(ApplicationMetaModel application, AnnotationKey key, AnnotationState added) {
Name type = key.getType();
if (type.toString().equals(BundleGen.class.getName())) {
ElementHandle.Package pkg = (ElementHandle.Package)key.getElement();
bundles.put(pkg, (String)added.get("value"));
}
}
@Override
public void processAnnotationRemoved(ApplicationMetaModel application, AnnotationKey key, AnnotationState removed) {
Name type = key.getType();
if (type.toString().equals(BundleGen.class.getName())) {
ElementHandle.Package pkg = (ElementHandle.Package)key.getElement();
bundles.remove(pkg);
}
}
The two callbacks are used to maintain a map with all the known annotations by the plugin. Indeed code generation
will not occur in those callbacks but in a later callback (prePassivate
). The annotation key object provides
access to:
-
the element declaring the annotation, in our case it is an
ElementHandle.Package
since our annotation can only target Java packages -
the annotation type, we use it to check this is our annotation: in our case the check is here for educational purpose since we only register interest for the
@BundleGen
annotation
The annotation state is a map containing the various annotation members: we retrieve the value
member that is
the name of the resource bundle we will use later.
Generating the bundle source
Until now we focused on the plugin structure and the lifecycles of the framework. This part will show the implementation
of the prePassivate
callback we mentionned earlier:
prePassivate
callback @Override
public void prePassivate(ApplicationMetaModel application) {
for (Map.Entry<ElementHandle.Package, String> entry : bundles.entrySet()) {
ElementHandle.Package pkg = entry.getKey();
String bundleName = entry.getValue();
processBundle(application, pkg, bundleName);
}
}
The
prePassivate
method has the application
argument which is the current application being processed it
can be used to access the overral application structure. The implementation simply iterates the
bundles
map of the plugin calling the processBundle
method:
processBundle
method private void processBundle(ApplicationMetaModel application, ElementHandle.Package pkg, String bundleName) {
ProcessingContext context = application.getProcessingContext();
PackageElement pkgElt = context.get(pkg);
Properties properties = loadBundle(context, pkg, bundleName);
if (properties == null) {
throw BUNDLE_NOT_FOUND.failure(pkgElt, bundleName);
} else {
try {
generateBundleClass(context, properties, pkgElt, bundleName);
}
catch (IOException e) {
throw CANNOT_CREATE_BUNDLE.failure(pkgElt, bundleName).initCause(e);
}
}
}
The processBundle
does three things:
-
it uses processing context to load the resource bundle via the
loadBundle
method -
when the bundle is found, the class generation is delegated to the
generateBundleClass
method -
error handling
-
when the bundle is not found, the plugin raises a
BUNDLE_NOT_FOUND
exception -
when the
generateBundleClass* method thows an +IOException
theCANNOT_CREATE_BUNDLE
exception is raised
-
Raising an error is done via the juzu.impl.compiler.ProcessingException
. Providing the package
element is important as it allows the compiler to do a precise error reporting when showing the error.
Such exception can be created directly or via the juzu.impl.compiler.MessageCode
class: it uses
the ProcessingException
to create the notion of message code:
public static final MessageCode BUNDLE_NOT_FOUND = new MessageCode("BUNDLE_NOT_FOUND", "The bundle %1$s cannot be resolved");
public static final MessageCode CANNOT_CREATE_BUNDLE = new MessageCode("CANNOT_CREATE_BUNDLE", "The bundle %1$s cannot be created");
The loadBundle
implementation uses the current ProcessingContext
to load
the bundle property file the user will provide in the package containing the @BundleGen
annotation:
loadBundle
method private Properties loadBundle(ProcessingContext context, ElementHandle.Package pkg, String bundleName) {
Path.Absolute absolute = pkg.getPackageName().resolve(bundleName + ".properties");
FileObject bundle = context.resolveResourceFromSourcePath(pkg, absolute);
if (bundle != null) {
try {
InputStream in = bundle.openInputStream();
Properties properties = new Properties();
properties.load(in);
return properties;
}
catch (IOException e) {
context.log(Level.SEVERE, "Could not load resource bundle", e);
}
}
return null;
}
Finally let’s wrap up the bundle processing by looking at the generateBundleClass
method which generates the
Java class:
generateBundleClass
method private void generateBundleClass(ProcessingContext context, Properties properties, PackageElement pkgElt, String bundleName) throws IOException {
String fqn = pkgElt.getQualifiedName() + "." + bundleName;
JavaFileObject source = context.createSourceFile(fqn, pkgElt);
PrintWriter writer = new PrintWriter(source.openWriter());
writer.println("package " + pkgElt.getQualifiedName() + ";");
writer.println("import examples.plugin.bundlegen.impl.BundleResolver;");
writer.println("public class " + bundleName + " {");
for (String key : properties.stringPropertyNames()) {
writer.println("public static String " + key + "() {");
writer.println("return BundleResolver.resolve(" + bundleName + ".class, \"" + key + "\");");
writer.println("}");
}
writer.println("}");
writer.close();
}
The implementation is quite straightforward, it creates a source file that returns a JavaFileObject
whose
writer is used to to print the source code of the bundle. The generated bundle class contains one method
for each property of resource bundle.
Testing the plugin
The last part of the bundlegen
tutorial explains how to test a compilation plugin. We will
use create a mock application from its sources, this way we will test the plugin works from the source
files to the runtime.
We will study three tests each one focusing on a different aspect of the plugin usage:
-
successful generation of the bundle class
-
runtime resolving of the bundle via the bundle class
-
correct error reporting when the bundle properties file does not exist
Testing the bundle generation
For this part, we will only use the Juzu compiler interface which is a wrapper of the underlying Java compiler that can be used for writing unit tests. This wrapper allows us to compile an application from its sources. Our test application contains the following package declaration:
@BundleGen("mybundle")
@Application
package examples.app1;
The resource bundle properties contains a single property:
abc=def
The sources of the application are located in the resources of the project, the only thing needed for the compiler to resolve the source is the package of the application.
testGenerateBundle
test @Test
public void testGenerateBundle() throws Exception {
CompilerAssert<File, File> compilerAssert = compiler("examples.app1"); //1
compilerAssert.assertCompile(); //2
ReadWriteFileSystem<File> output = compilerAssert.getClassOutput(); //3
ClassLoader loader = new URLClassLoader(new URL[]{ output.getURL() }); //4
Class<?> bundleClass = loader.loadClass("examples.app1.mybundle"); //5
Method m = bundleClass.getDeclaredMethod("abc"); //6
assertEquals(String.class, m.getReturnType());
}
1 | Create the compiler from the tested application package |
2 | Triggers compilation |
3 | Get the file system where the compiler created the compiled classes |
4 | Create a classloader for loading the bundle class |
5 | Load the bundle class |
6 | Assert the bundle class structure |
Testing the plugin failure
In the previous section we tested the expected behavior of the plugin when it works, we also need to test
the behavior of the plugin when the bundle does not exist. The absence of the bundle properties file
will raise a compilation error with the BUNDLE_NOT_FOUND
message code. By default the compiler will
use the message defined in the constant, however we can set the compiler in the special formalErrorReporting
mode that will use instead the message code value, making easier to write our test:
testBundleNotFound
test @Test
public void testBundleNotFound() throws Exception {
CompilerAssert<File, File> compilerAssert = compiler("examples.app2");
compilerAssert.formalErrorReporting(); //1
List<CompilationError> errors = compilerAssert.failCompile(); //2
assertEquals(1, errors.size()); //3
CompilationError error = errors.get(0);
assertEquals(BundleGenMetaModelPlugin.BUNDLE_NOT_FOUND, error.getCode()); //4
assertEquals(Collections.singletonList("mybundle"), error.getArguments());
File source = error.getSourceFile();
assertEquals("package-info.java", source.getName());
assertEquals("app2", source.getParentFile().getName());
}
1 | Set the formalErrorReporting mode |
2 | We call now the failCompile that returns a list of compilation errors |
3 | We shall have a single error |
4 | Among other checks we assert that the BUNDLE_NOT_FOUND code was used |
Testing the bundle resolution
Last but not last, we will test now the runtime behavior. A very simple controller will test the behavior at runtime:
public class Controller {
@View
public Response index() {
return Response.ok("<abc>" + mybundle.abc() + "</abc>");
}
}
The previous test only required a compiler, now we need to execute our application. Juzu provides a mocking framework that takes an application, compiles and then run it.
testResolveBundle
test @Test
public void testResolveBundle() throws Exception {
MockApplication<File> application = application(InjectorProvider.GUICE, "examples.app1"); //1
application.init(); //2
MockClient client = application.client();
MockViewBridge view = client.render(); //3
String markup = view.assertStringResponse();
assertEquals("<abc>def</abc>", markup); //4
}
1 | Create a mock application |
2 | The init step compiles and starts the application |
3 | Render the application, this invokes the index method of the application |
4 | Assert the returned application markup is correct |
Metrics plugin tutorial
The Metrics plugin tutorial provides guidance for the implementation of a Juzu service. This plugin provides an integration of the Metrics library in Juzu. Metrics is an easy to use library for reporting and publishing metrics.
In this section we will study three Juzu runtime aspects:
-
creation of a Juzu service
-
intercepting a Juzu request
-
binding a bean in the IOC container
Creating a Juzu service
A Juzu service is a class extending the juzu.impl.plugin.application.ApplicationService
abstract class. The
service must be declared in a +META-INF/services/juzu.impl.plugin.application.ApplicationService_ file:
META-INF/services/juzu.impl.plugin.application.ApplicationService
service declarationexamples.plugin.metrics.MetricsService
Our service constructor must be a public no argument constructor, it calls the super constructor with the service name metrics:
public MetricsService() {
super("metrics");
registry = new MetricRegistry();
responses = registry.timer("juzu.responses");
}
The constructor initializes two fields:
-
the
registry
field, a Metrics registry that holds all the metrics, this registry will be bound in the IOC container so an application or another service can be injected with this registry for publishing custom metrics -
the
responses
field, a Metrics timer, bound in registry that will accumulate request statistics
Service initialization
After the service is created, Juzu will call its init
method to perform the initialization step. The init
method can return a ServiceDescriptor
that will instruct Juzu to load the service. The service can chose
to return null instead of the descriptor, the null value tells Juzu to discard the service instead.
The service descriptor contains a list of BeanDescriptor
classes: this is the list of the beans the service
wants to register in the IOC container. In our case we want to expose the Metrics registry as a singleton
bean so it can be injected in the whole application:
@Override
public ServiceDescriptor init(ServiceContext context) throws Exception {
Provider<MetricRegistry> registryProvider = new Provider<MetricRegistry>() {
@Override
public MetricRegistry get() {
return registry;
}
}; //1
BeanDescriptor registryDescriptor = BeanDescriptor.createFromProvider( //2
MetricRegistry.class,
Scope.SINGLETON,
Collections.<Annotation>emptyList(),
registryProvider);
return new ServiceDescriptor(Arrays.asList(registryDescriptor)); //3
}
1 | Create a javax.inject.Provider that wraps the registry field |
2 | Create a singleton bean descriptor |
3 | Return the service descriptor with the registry bean |
After the service is initialized, the service will be started, this event can be intercepted with the
javax.annotation.PostConstruct
callback: our service will use it to bind the registry into the static
SharedMetricRegistries
exposed by Metrics:
@PostConstruct
public void start() {
SharedMetricRegistries.add(application.getPackageName(), registry);
}
The registry is bound under the application package name, making sure that registries will be namespaced correctly to avoid collisions.
Intercepting the request
We have now a fully initiliazed Metrics registry and timer, let’s see how to intercept the Juzu requests
to collect statistics in this timer. Intercepting a request is done by implementing the juzu.impl.request.RequestFilter
interface for a specific request stage.
The application request is indeed decomposed into four stages:
-
unmarshalling : unmarshalls the request entity when there is one
-
handler : determine the handler arguments and get the application handler from the IOC container
-
lifecycle : trigger the controller lifecycle and calls the
juzu.request.RequestLifeCycle
interface when necessary -
invoke : performs the handler method invocation on the controller
Our service can use mostly any of these stage as it just wants to count the number of requests, so it will use
the invoke stage and thus our service implements the RequestFilter<Stage.Invoke>
interface:
@Override
public Response handle(Stage.Invoke argument) {
final Timer.Context context = responses.time();
try {
return argument.invoke();
} finally {
context.stop();
}
}
The implementation is quite trivial, it uses the responses
timer to collect the request statistic.
Testing the plugin
We will test the plugin via the MockApplication
object provided by Juzu, the test application is quite simple with
a single controller:
public class Controller {
@Inject
public Controller(MetricRegistry registry) {
registry.meter("custom");
}
@View
public Response index() {
return Response.ok("ok");
}
}
-
the constructor is injected with the registry and it publishes a custom meter
-
the
index
method handles the application request and returns a simple ok value
Our test will assert three things:
-
the Metrics registry is published in the
SharedMetricRegistries
-
the Metrics registry recorded the request succesfully
-
the Metrics registry can be used by the application
@Test
public void testTimer() throws Exception {
SharedMetricRegistries.clear(); //1
MockApplication<File> application = application(InjectorProvider.GUICE, "examples.metrics");//2
application.init();
MockClient client = application.client();
MockViewBridge view = client.render(); //3
assertEquals("ok", view.assertStringResponse());
assertEquals(Collections.singleton("examples.metrics"), SharedMetricRegistries.names()); //4
MetricRegistry registry = SharedMetricRegistries.getOrCreate("examples.metrics");
Timer timer = registry.getTimers().get("juzu.responses"); //5
assertEquals(1, timer.getCount());
Meter meter = registry.getMeters().get("custom"); //6
assertNotNull(meter);
}
1 | Clear the SharedMetricRegistries |
2 | Create the mock application |
3 | Invoke the application |
4 | Check our registry was published under the application package name |
5 | Get the timer and check it recorded the statistic |
6 | Check the application used the registry via injection succesfully |