This week, I had an interesting question on Twitter: "How in Vaadin do you add a lang
attribute to the html
element?", like this:
<html lang="fr">
While it’s quite easy to customize individual components on the page, the outer html tag is outside our control. In this article, I’ll describe a possible solution to this problem. Note that I think this is too specialized for morevaadin.com. However, this should still be my reference site for all things Vaadin.
My first (wrong) thought, was that the html
element is generated by the UI widget client-side.
It’s not the case - as there’s no such widget.
In order to find the answer, I had to understand how the framework works.
Here’s a short summary of the sequence when one makes a request to a Vaadin application.
Basically, the Vaadin Servlet delegates to the Vaadin Service. In turn, the service loops over its internal list of Request Handlers until one of them is able to handle the request. By default, the Vaadin Service registers the following handlers:
- ConnectorResourceHandler
- UnsupportedBrowserHandler: returns a specific message if the browser is not supported by Vaadin
- UidlRequestHandler: handles RPC communication between client and server
- FileUploadHandler: handles file uploads achieved with the
FileUpload
component - HeartbeatHandler: handles heartbeat requests
- PublishedFileHandler
- SessionRequestHandler: delegates in turn to request handlers that have been registered in the session
This is implemented in VaadinService.createRequestHandlers()
.
However, notice that VaadinService
is abstract.
There’s a subtle changed introduced in the concrete VaadinServletService
subclass that is used.
It registers a new request handler - the ServletBootstrapHandler
, in order for Vaadin to run transparently in both a servlet and a portlet context.
In this later case, only a fragment:
The servlet boostrap handler is responsible to generate the initial HTML page at the start of the application, including the desired outer html
tag.
Next requests to the application will just update part of the page via AJAX, but the outside HTML won’t change.
Thus, this is the class that needs to be updated if one wishes to add a lang
attribute.
A quick glance at the class makes it very clear that it’s quite hard to extend.
Besides, it delegates HTML generation to the JSoup and more precisely to the Document.createShell()
static method.
At this point, you have 3 options:
- Forget about it, what is it worth anyway?
- Rewrite a large portion of the BootstrapHandler and add it before the BootstrapHandler in the handlers sequence
- Be lazy and apply Ockham’s razor: just use a simple AOP aspect
In order to be of any use, I chose the latest option. It’s quite straightforward and very concise, you only need the following steps.
Create the aspect itself. Note that I’m using Kotlin to be coherent with the existing project but Java or any other JVM-based language would be the same.
@Aspect
open class UpdateLangAspect {
@Pointcut("execution(static * org.jsoup.nodes.Document.createShell(..))")
fun callDocumentCreateShell(): Unit {}
@AfterReturning(pointcut = "callDocumentCreateShell()", returning = "document")
fun setLangAttribute(document: Document): Unit {
document.childNode(0).attr("lang", "fr")
}
}
As the method to instrument is static, it’s not possible to use simple Spring proxies, but we need AspectJ class instrumentation via LTW. In order to activate it in Spring Boot, I just have to update the application.properties file:
spring.aop.proxy-target-class=true
Also, one has to tell Aspect’s weaver what needs to be instrumented in the META-INF/aop.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<aspectj>
<aspects>
<aspect name="ch.frankel.blog.bootvaadinkotlin.UpdateLangAspect" />
</aspects>
<weaver options="-XnoInline -Xlint:ignore -verbose -showWeaveInfo">
<include within="org.jsoup.nodes.Document" />
<include within="ch.frankel.blog.bootvaadinkotlin.UpdateLangAspect" />
</weaver>
</aspectj>
The first section is about the aspect, the second is about what classes need to be instrumented. Note that weaving section needs also to reference the aspect too.
Finally, one has to update the POM so that launching the application through the Spring Boot Maven plugin will also use the aspect.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<agent>${settings.localRepository}/org/aspectj/aspectjweaver/1.8.8/aspectjweaver-1.8.8.jar</agent>
</configuration>
</plugin>
For sake of completeness, the code is available on Github with the manage-langv1 tag.
At this point, generating the page will display the desired change. Job done!