Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bom/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,17 @@
<artifactId>quarkus-scala-deployment</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute-deployment</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Quarkus test dependencies -->

Expand Down
27 changes: 27 additions & 0 deletions bom/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,16 @@
<artifactId>quarkus-test-kubernetes-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
<version>${project.version}</version>
</dependency>

<!-- External dependencies -->

Expand Down Expand Up @@ -2458,6 +2468,23 @@
<artifactId>quarkus-vertx-graphql</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Qute -->
<dependency>
<groupId>io.quarkus.qute</groupId>
<artifactId>qute-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus.qute</groupId>
<artifactId>qute-generator</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus.qute</groupId>
<artifactId>qute-rxjava</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ public final class FeatureBuildItem extends MultiBuildItem {
public static final String REACTIVE_MYSQL_CLIENT = "reactive-mysql-client";
public static final String NEO4J = "neo4j";
public static final String OIDC = "oidc";
public static final String QUTE = "qute";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't advertise the RESTEasy one? There might be good reason not to, just asking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A user will see [qute, resteasy]... I don't think there's a reason to add qute-resteasy or resteasy-qute.

Copy link
Contributor

@gastaldi gastaldi Nov 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: Is it possible to automatically include qute-resteasy only if qute and resteasy are added to the project?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this could be done automatically ATM. But yes, it would make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... it's not that obvious to me.

If you have [qute, resteasy], that doesn't mean that you have the RESTEasy Qute support. The dependency could be missing and nothing will work.

So you could have two applications returning the exact same set of extensions and behaving differently. This is a problem IMHO.

And even for us or for support, when they will ask for the extension list, they will have a partial information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, ignoring my comment doesn't magically resolve the issue ;)

I really think we should make it easy for support to know the list of installed extensions. As explained above, right now, they might have the same list of extensions and totally different behaviors.

RESTEasy Qute is a user exposed extension so it should be displayed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, ignoring my comment doesn't magically resolve the issue ;)

That's a pity ;-). Well, I didn't know we have resteasy-jsonb and others. I will add it but it looks weird. Just my 2c.

public static final String RESTEASY = "resteasy";
public static final String RESTEASY_JACKSON = "resteasy-jackson";
public static final String RESTEASY_JAXB = "resteasy-jaxb";
public static final String RESTEASY_JSONB = "resteasy-jsonb";
public static final String RESTEASY_QUTE = "resteasy-qute";
public static final String REST_CLIENT = "rest-client";
public static final String SCALA = "scala";
public static final String SCHEDULER = "scheduler";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
Expand Down Expand Up @@ -460,16 +459,21 @@ private Path getClassInApplicationClassPaths(String name) {

private URL findApplicationResource(String name) {
Path resourcePath = null;

// Resource names are always separated by the "/" character.
// Here we are trying to resolve those resources using a filesystem
// Path, so we replace the "/" character with the filesystem
// specific separator before resolving
if (File.separatorChar != '/') {
name = name.replace('/', File.separatorChar);
}
for (Path i : applicationClassDirectories) {
resourcePath = i.resolve(name);
if (Files.exists(resourcePath)) {
break;
}
}
try {
return resourcePath != null && Files.exists(resourcePath) ? resourcePath.toUri()
.toURL() : null;
return resourcePath != null && Files.exists(resourcePath) ? resourcePath.toUri().toURL() : null;
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Expand Down Expand Up @@ -522,14 +526,15 @@ private ProtectionDomain createDefaultProtectionDomain(Path applicationClasspath
URL url = null;
if (applicationClasspath != null) {
try {
URI uri = new URI("file", null, applicationClasspath.toString(), null);
URI uri = applicationClasspath.toUri();
url = uri.toURL();
} catch (URISyntaxException | MalformedURLException e) {
log.error("URL codeSource location for path " + applicationClasspath + " could not be created.");
} catch (MalformedURLException e) {
log.error("URL codeSource location for path " + applicationClasspath + " could not be created.", e);
}
}
CodeSource codesource = new CodeSource(url, (Certificate[]) null);
ProtectionDomain protectionDomain = new ProtectionDomain(codesource, null, this, null);
return protectionDomain;
}

}
272 changes: 272 additions & 0 deletions docs/src/main/asciidoc/qute.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
////
This guide is maintained in the main Quarkus repository
and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc
////
= Qute Templating Engine

include::./attributes.adoc[]

Qute is a templating engine designed specifically to meet the Quarkus needs.
The usage of reflection is minimized to reduce the size of native images.
The API combines both the imperative and the non-blocking reactive style of coding.
In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible.
Furthermore, we try to detect most of the template problems at build time.
In this guide, you will learn how to easily render templates in your application.

[NOTE]
====
This extension is considered `preview`.
API or configuration properties might change as the extension matures.
Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker].
====

== Hello World with JAX-RS

If you want to use Qute in your JAX-RS application, you need to add the `quarkus-qute-resteasy` extension first.
In your `pom.xml` file, add:

[source,xml]
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
</dependency>
----

We'll start with a very simple template:

.hello.txt
----
Hello {name}! <1>
----
<1> `{name}` is a value expression that is evaluated when the template is rendered.

NOTE: By default, all files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode.

Now let's inject the "compiled" template in the resource class.

.HelloResource.java
[source,java]
----
package org.acme.quarkus.sample;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("hello")
public class HelloResource {

@Inject
Template hello; <1>

@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return hello.data("name", name); <2> <3>
}
}
----
<1> If there is no `@ResourcePath` qualifier provided, the field name is used to locate the template. In this particular case, we're injecting a template with path `templates/hello.txt`.
<2> `Template.data()` returns a new template instance that can be customized before the actual rendering is triggered. In this case, we put the name value under the key `name`. The data map is accessible during rendering.
<3> Note that we don't trigger the rendering - this is done automatically by a special `ContainerResponseFilter` implementation.

If your application is running, you can request the endpoint:

[source, shell]
```
$ curl -w "\n" http://localhost:8080/hello?name=Martin
Hello Martin!
```

== Parameter Declarations and Template Extension Methods

Qute has many useful features.
In this example, we'll demonstrate two of them.
If you declare a *parameter declaration* in a template then Qute attempts to validate all expressions that reference this parameter and if an incorrect expression is found the build fails.
*Template extension methods* are used to extend the set of accessible properties of data objects.

Let's suppose we have a simple class like this:

.Item.java
[source,java]
----
public class Item {
public String name;
public BigDecimal price;
}
----

And we'd like to render a simple HTML page that contains the item name, price and also a discounted price.
The discounted price is sometimes called a "computed property".
We will implement a template extension method to render this property easily.
Let's start again with the template:

.item.html
[source,html]
----
{@org.acme.Item item} <1>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{item.name}</title> <2>
</head>
<body>
<h1>{item.name}</h1>
<div>Price: {item.price}</div>
{#if item.price > 100} <3>
<div>Discounted Price: {item.discountedPrice}</div> <4>
{/if}
</body>
</html>
----
<1> Optional parameter declaration. Qute attempts to validate all expressions that reference the parameter `item`.
<2> This expression is validated. Try to change the expression to `{item.nonSense}` and the build should fail.
<3> `if` is a basic control flow section.
<4> This expression is also validated against the `Item` class and obviously there is no such property declared. However, there is a template extension method declared on the `ItemResource` class - see below.

Finally, let's create a resource class.

.ItemResource.java
[source,java]
----
package org.acme.quarkus.sample;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("item")
public class ItemResource {

@Inject
ItemService service;

@Inject
Template item; <1>

@GET
@Path("{id}")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get(@PathParam("id") Integer id) {
return item.data("item", service.findItem(id)); <2>
}

@TemplateExtension <3>
static BigDecimal discountedPrice(Item item) {
return item.price.multiply(new BigDecimal("0.9"));
}
}
----
<1> Inject the template with path `templates/item.html`.
<2> Make the `Item` object accessible in the template.
<3> A static template extension method can be used to add "computed properties" to a data class. The class of the first parameter is used to match the base object and the method name is used to match the property name.

== Rendering Periodic Reports

Templating engine could be also very useful when rendering periodic reports.
You'll need to add the `quarkus-scheduler` and `quarkus-qute` extensions first.
In your `pom.xml` file, add:

[source,xml]
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
----

Let's suppose the have a `SampleService` bean whose `get()` method returns a list of samples.

.Sample.java
[source,java]
----
public class Sample {
public boolean valid;
public String name;
public String data;
}
----

The template is simple:

.report.html
[source,html]
----
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Report {now}</title>
</head>
<body>
<h1>Report {now}</h1>
{#for sample in samples} <1>
<h2>{sample.name ?: 'Unknown'}</h2> <2>
<p>
{#if sample.valid}
{sample.data}
{#else}
<strong>Invalid sample found</strong>.
{/if}
</p>
{/for}
</body>
</html>
----
<1> The loop section makes it possible to iterate over iterables, maps and streams.
<2> This value expression is using the https://en.wikipedia.org/wiki/Elvis_operator[elvis operator] - if the name is null the default value is used.

[source,java]
.ReportGenerator.java
----
package org.acme.quarkus.sample;

import javax.inject.Inject;

import io.quarkus.qute.Template;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.scheduler.Scheduled;

public class ReportGenerator {

@Inject
SampleService service;

@ResourcePath("reports/v1/report_01") <1>
Template report;

@Scheduled(cron="0 30 * * * ?") <2>
void generate() {
String result = report
.data("samples", service.get())
.data("now", java.time.LocalDateTime.now())
.render(); <3>
// Write the result somewhere...
}
}
----
<1> In this case, we use the `@ResourcePath` qualifier to specify the template path: `templates/reports/v1/report_01.html`.
<2> Use the `@Scheduled` annotation to instruct Quarkus to execute this method on the half hour. For more information see the link:scheduler[Scheduler] guide.
<3> The `TemplateInstance.render()` method triggers rendering. Note that this method blocks the current thread.

[[qute-configuration-reference]]
== Qute Configuration Reference

include::{generated-dir}/config/quarkus-qute.adoc[leveloffset=+1, opts=optional]
Loading