diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..72af12c56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +qute/** text=input diff --git a/README.md b/README.md index 2d69e58e28..30674700a3 100644 --- a/README.md +++ b/README.md @@ -936,7 +936,10 @@ in order to instantiate these templates by your self (as an example). need it to be deployed into ocp or some other platform. ### `Qute` -Coverage for Qute template engine +Coverage for Qute template engine. +Module `qute/synchronous` contains coverage for Qute templating and integration with RESTEasy. +Module `qute/reactive` contains coverage for Qute templating and integration with RESTEasy reactive. +Module `qute/multimodule` provides coverage for complicated issue of having localised messages in separate modules. ### `spring/spring-data` - Spring Data JPA: CRUD repository operation (default and custom), mapped superclass, query over embedded camelCase field, HTTP response filter. diff --git a/pom.xml b/pom.xml index ae1cc29d01..592fd90ddf 100644 --- a/pom.xml +++ b/pom.xml @@ -395,6 +395,8 @@ logging/jboss cache/caffeine qute/multimodule + qute/synchronous + qute/reactive helm/helm-minimum diff --git a/qute/reactive/pom.xml b/qute/reactive/pom.xml new file mode 100644 index 0000000000..4ceac8c5d6 --- /dev/null +++ b/qute/reactive/pom.xml @@ -0,0 +1,19 @@ + + + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../../pom.xml + + 4.0.0 + qute-reactive + jar + Quarkus QE TS: Reactive Qute + + + io.quarkus + quarkus-resteasy-reactive-qute + + + diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/Application.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/Application.java new file mode 100644 index 0000000000..c60f31dc2a --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/Application.java @@ -0,0 +1,230 @@ +package io.quarkus.ts.qute; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.Location; +import io.quarkus.qute.Qute; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.i18n.Localized; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@Path("") +public class Application { + + @Inject + Template basic; + + @Location("1.i18n.html") + Template multiLanguage; + + @Inject + Engine engine; + + @Localized("he") + Messages hebrew; + + @GET + @Path("/basic") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance base() { + return basic.data("server", "Quarkus"); + } + + @GET + @Path("/location") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance located() { + return multiLanguage.data("server", "Quarkus"); + } + + @GET + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response engine(@PathParam("name") String name) { + final Template template = engine.getTemplate(name); + if (template == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(template.data("server", name + " engine")).build(); + } + + @POST + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response registerNew(@PathParam("name") String name, String body) { + Response.ResponseBuilder result; + if (engine.getTemplate(name) == null) { + engine.putTemplate(name, engine.parse(body)); + result = Response.created(getUri("/engine/" + name)); + } else { + result = Response.status(Response.Status.CONFLICT); + } + return result.build(); + } + + @PUT + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response register(@PathParam("name") String name, String body) { + Template existing = engine.putTemplate(name, engine.parse(body)); + if (existing == null) { + return Response.created(getUri("/engine/" + name)).build(); + } else { + return Response.noContent().build(); + } + } + + @DELETE + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response delete(@PathParam("name") String name) { + engine.removeTemplates(templateName -> templateName.equals(name)); + return Response.ok().build(); + } + + @GET + @Path("/format") + @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) + public String format(@QueryParam("name") String name) { + return Qute.fmt("This page is rendered for \"{}\" by Qute", name); + } + + @GET + @Path("/format-advanced") + @Produces(MediaType.TEXT_HTML) + public String formatHtml(@QueryParam("name") String name) { + return Qute.fmt("This text is fluently rendered for \"{name}\" by Qute") + .data("name", name) + .render(); + } + + @GET + @Path("/book") + @Produces(MediaType.TEXT_HTML) + public CompletionStage book() { + Book musketeers = new Book("The Three Musketeers", "Alexandre Dumas", "d'Artagnan", "Athos", "Porthos", "Aramis"); + return engine.getTemplate("expressions") + .data("server", "engine") + .data("book", musketeers) + .renderAsync(); + } + + @GET + @Path("/encoding") + @Produces(MediaType.TEXT_HTML) + public Uni encoding() { + return engine.getTemplate("нелатынь") + .data("English", "hello") + .data("česky", "čau") + .data("по-русски", "привет") + .data("בעברית", "שלום") + .createUni(); + } + + @GET + @Path("/map") + @Produces(MediaType.TEXT_HTML) + public Multi map(@QueryParam("name") @DefaultValue("islands") String region) { + Map map = new HashMap<>(); + if (region.equals("islands")) { + map.put("Tasmania", "Hobart"); + map.put("Java", "Jakarta"); + map.put("The Great Britain", "London"); + } + return engine + .getTemplate("maps") + .data("map", map) + .createMulti(); + } + + @GET + @Path("/inheritance") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance inheritance(@QueryParam("name") @DefaultValue("detail") String template) { + return engine.getTemplate(template).instance(); + } + + @GET + @Path("/annotated") + public TemplateInstance annotated() { + final Fish trout = new Fish("trout"); + return engine + .getTemplate("annotations") + .data("fish", trout) + .data("wrapper", new StringWrapper("A quick brown fox jumps over the lazy dog")); + } + + @GET + @Path("/enums/{city}") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance enums(@PathParam("city") String city) { + final City destination = City.valueOf(city.toUpperCase()); + return engine.parse("Good news, we will{#if city == City:BRUGES} not{/if} spend a week in {city.naturalName}!") + .data("city", destination); + } + + @GET + @Path("/message/{locale}") + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance message(@PathParam("locale") String tag) { + return engine + .parse(String.format("{greeting:hello('%s')}", "Dr. Livingstone")) + .instance() + .setAttribute("locale", Locale.forLanguageTag(tag)); + } + + @GET + @Path("/message/long/{locale}") + @Produces(MediaType.TEXT_PLAIN) + public Uni longMessage(@PathParam("locale") String tag) { + return engine + .parse(String.format("{greeting:long_hello('%s')}", "Dr. Livingstone")) + .instance() + .setAttribute("locale", Locale.forLanguageTag(tag)) + .createUni(); + } + + @GET + @Path("/message/") + @Produces(MediaType.TEXT_PLAIN) + public String injectedLocalization() { + return hebrew.hello("אדם"); + } + + private static URI getUri(String path) { + final Config system = ConfigProvider.getConfig(); + final String host = system.getValue("quarkus.http.host", String.class); + final Integer port = system.getValue("quarkus.http.port", Integer.class); + try { + return new URL("http", host, port, path).toURI(); + } catch (URISyntaxException | MalformedURLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/Book.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/Book.java new file mode 100644 index 0000000000..cbf8df59d0 --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/Book.java @@ -0,0 +1,16 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData +public class Book { + public final String title; + public final String author; + public final String[] characters; + + public Book(String title, String author, String... characters) { + this.title = title; + this.author = author; + this.characters = characters; + } +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/City.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/City.java new file mode 100644 index 0000000000..10495db47d --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/City.java @@ -0,0 +1,17 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum City { + BRUGES, + DUBLIN, + MOMBASA; + + public String naturalName() { + String full = this.name(); + String firstLetter = full.substring(0, 1); + String rest = full.substring(1).toLowerCase(); + return firstLetter + rest; + } +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/Fish.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/Fish.java new file mode 100644 index 0000000000..0ce7edf1fa --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/Fish.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData +public class Fish { + private final String name; + + public Fish(String name) { + this.name = name; + } + + public String saySomething() { + return String.format("This %s stays silent", this.name); + } + + public String slap(String user, String target) { + return String.format("%s slaps %s around a bit with a large %s", user, target, this.name); + } +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/Messages.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/Messages.java new file mode 100644 index 0000000000..c47ca810dc --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/Messages.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; + +@MessageBundle("greeting") +public interface Messages { + @Message("Hello, {name}!") + String hello(String name); + + @Message("Hello, {name}! \n How are you, {name}?") + String long_hello(String name); + +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/SpanishMessages.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/SpanishMessages.java new file mode 100644 index 0000000000..002d919165 --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/SpanishMessages.java @@ -0,0 +1,12 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.i18n.Localized; +import io.quarkus.qute.i18n.Message; + +@Localized("es") +public interface SpanishMessages extends Messages { + + @Override + @Message("Hola, {name}!") + String hello(String name); +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/StringWrapper.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/StringWrapper.java new file mode 100644 index 0000000000..641cc5b74f --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/StringWrapper.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData(target = String.class) +@TemplateData //required for native +public class StringWrapper { + public final String content; + + public StringWrapper(String content) { + this.content = content; + } +} diff --git a/qute/reactive/src/main/java/io/quarkus/ts/qute/Variables.java b/qute/reactive/src/main/java/io/quarkus/ts/qute/Variables.java new file mode 100644 index 0000000000..3776afb106 --- /dev/null +++ b/qute/reactive/src/main/java/io/quarkus/ts/qute/Variables.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateGlobal; + +@TemplateGlobal +public class Variables { + static final int airspeedVelocityOfAnUnladenSwallow = 11; + + @TemplateGlobal(name = "random") + static int getRandomNumber() { + return 5; + } +} diff --git a/qute/reactive/src/main/resources/messages/greeting_cs.properties b/qute/reactive/src/main/resources/messages/greeting_cs.properties new file mode 100644 index 0000000000..21b0dd4383 --- /dev/null +++ b/qute/reactive/src/main/resources/messages/greeting_cs.properties @@ -0,0 +1 @@ +hello=Ahoj, {name}! diff --git a/qute/reactive/src/main/resources/messages/greeting_he.properties b/qute/reactive/src/main/resources/messages/greeting_he.properties new file mode 100644 index 0000000000..270f414488 --- /dev/null +++ b/qute/reactive/src/main/resources/messages/greeting_he.properties @@ -0,0 +1,3 @@ +hello=שלם, {name}! +long_hello=שלם, {name}, \ + מה נשמע? diff --git a/qute/reactive/src/main/resources/templates/1.i18n.html b/qute/reactive/src/main/resources/templates/1.i18n.html new file mode 100644 index 0000000000..1ee7cb132e --- /dev/null +++ b/qute/reactive/src/main/resources/templates/1.i18n.html @@ -0,0 +1,3 @@ + +

This page is fetched and rendered by {server}

+ diff --git a/qute/reactive/src/main/resources/templates/annotations.html b/qute/reactive/src/main/resources/templates/annotations.html new file mode 100644 index 0000000000..6877d0805b --- /dev/null +++ b/qute/reactive/src/main/resources/templates/annotations.html @@ -0,0 +1,5 @@ +{fish.slap("Joe", "Seligman")}
+{fish.saySomething} +{wrapper.content.replace("o","()")} +Airspeed velocity of an unladen swallow is {airspeedVelocityOfAnUnladenSwallow} m/s +This random number was chosen by a roll of dice: {random} diff --git a/qute/reactive/src/main/resources/templates/base.html b/qute/reactive/src/main/resources/templates/base.html new file mode 100644 index 0000000000..b50383829a --- /dev/null +++ b/qute/reactive/src/main/resources/templates/base.html @@ -0,0 +1,10 @@ + + +{#insert title}A poem{/} + +{#insert first}{/} +{#insert}{/} +{#insert}Empty body!{/} +{#insert last}{/} + + diff --git a/qute/reactive/src/main/resources/templates/basic.html b/qute/reactive/src/main/resources/templates/basic.html new file mode 100644 index 0000000000..a4a04a3305 --- /dev/null +++ b/qute/reactive/src/main/resources/templates/basic.html @@ -0,0 +1,3 @@ + +

This page is rendered by {server}

+ diff --git a/qute/reactive/src/main/resources/templates/detail.html b/qute/reactive/src/main/resources/templates/detail.html new file mode 100644 index 0000000000..853386b025 --- /dev/null +++ b/qute/reactive/src/main/resources/templates/detail.html @@ -0,0 +1,6 @@ +{#include base} +{#title}Auguries of Innocence{/title} +{#first}Every Morn and every Night
{/first} +{#last}Some are Born to Endless Night
{/last} +Some are Born to sweet delight
+{/include} diff --git a/qute/reactive/src/main/resources/templates/expressions.html b/qute/reactive/src/main/resources/templates/expressions.html new file mode 100644 index 0000000000..eaef33c4e3 --- /dev/null +++ b/qute/reactive/src/main/resources/templates/expressions.html @@ -0,0 +1,17 @@ + + +

This page is rendered by {server}

+The book is called {book.title} and is written by {book.['author']} +
+It has {book.characters.length} characters: +
+{#for character in book.characters} +{character_count}. {character}{#if character_hasNext} {#if character_odd }as well as{/if}{#if character_even }and also{/if}{/if}
+{/for} +
+At first, {book.characters.take(1).0} tries to fight on a duel with {book.characters.takeLast(3)[0]}, {book.characters.takeLast(3).get(1)} and {book.characters.takeLast(3).2}, but later they become friends.
+Here are some numbers:{#for i in 5} {i_index}{/for}
+{#for i in 5} +{#if i_isFirst}I have to say, that{#else}and{/if} this is {i_indexParity}{#if i_isLast}.{/if}
+{/for} + diff --git a/qute/reactive/src/main/resources/templates/maps.html b/qute/reactive/src/main/resources/templates/maps.html new file mode 100644 index 0000000000..c597d533f7 --- /dev/null +++ b/qute/reactive/src/main/resources/templates/maps.html @@ -0,0 +1,16 @@ + +{#if !map.isEmpty} +The capital of Tasmania is {map.Tasmania}, {map.['Java']} is a capital of Java and {map.get('The Great Britain')} is a capital of the UK. +{/if} + +
+{#for country in map.keySet} + {country_count}. {country}
+{/for} +I am a Java programmer and there are {#if map.isEmpty}zero{#else}{map.size}{/if} cities I know:
+{#for capital in map.values} + {capital_index}. {capital}
+{/for} + diff --git "a/qute/reactive/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" "b/qute/reactive/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" new file mode 100644 index 0000000000..0a0f2024b7 --- /dev/null +++ "b/qute/reactive/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" @@ -0,0 +1,9 @@ + + +

Englishmen say {English}

+

Češi říkají {česky}

+

Русские говорят {по-русски}

+

יהודים היגדים {בעברית}

+{|{česky} +{English}|} + diff --git a/qute/reactive/src/test/java/io/quarkus/ts/qute/DevModeQuteReactiveIT.java b/qute/reactive/src/test/java/io/quarkus/ts/qute/DevModeQuteReactiveIT.java new file mode 100644 index 0000000000..8e74422b6d --- /dev/null +++ b/qute/reactive/src/test/java/io/quarkus/ts/qute/DevModeQuteReactiveIT.java @@ -0,0 +1,48 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.DevModeQuarkusService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.DevModeQuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; +import io.restassured.response.Response; + +@QuarkusScenario +public class DevModeQuteReactiveIT { + private static final String FILE = "src/main/resources/templates/basic.html"; + private static final String RESERVE = "src/main/resources/templates/basic.html.bck"; + + @DevModeQuarkusApplication + static DevModeQuarkusService app = new DevModeQuarkusService(); + + @BeforeAll + static void beforeAll() { + app.copyFile(FILE, RESERVE); + } + + @Test + void changeTemplate() { + Response was = app.given().get("/basic"); + assertEquals("\n

This page is rendered by Quarkus

\n\n", was.body().asString()); + app.modifyFile(FILE, string -> string.replace("This page is rendered", "This page is being edited")); + AwaitilityUtils.untilAsserted(() -> { + Response changed = app.given().get("/basic"); + assertEquals("\n

This page is being edited by Quarkus

\n\n", changed.body().asString()); + }); + app.logs().assertContains("File change detected"); + app.logs().assertContains("Restarting quarkus due to changes in basic.html"); + + assertEquals("\n

This page is being edited by basic engine

\n\n", + app.given().get("/engine/basic").body().asString()); + } + + @AfterAll + static void afterAll() { + app.copyFile(RESERVE, FILE); + } +} diff --git a/qute/reactive/src/test/java/io/quarkus/ts/qute/MessageBundlesReactiveIT.java b/qute/reactive/src/test/java/io/quarkus/ts/qute/MessageBundlesReactiveIT.java new file mode 100644 index 0000000000..04626c6d76 --- /dev/null +++ b/qute/reactive/src/test/java/io/quarkus/ts/qute/MessageBundlesReactiveIT.java @@ -0,0 +1,75 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.response.Response; + +@QuarkusScenario +public class MessageBundlesReactiveIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Test + void smoke() { + Response response = app.given().get("message/en"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone!", response.body().asString()); + } + + @Test + void fromClass() { + Response response = app.given().get("message/es"); + assertEquals(200, response.statusCode()); + assertEquals("Hola, Dr. Livingstone!", response.body().asString()); + } + + @Test + void fromFile() { + Response response = app.given().get("message/cs"); + assertEquals(200, response.statusCode()); + assertEquals("Ahoj, Dr. Livingstone!", response.body().asString()); + } + + @Test + void rtl() { + Response response = app.given().get("message/he"); + assertEquals(200, response.statusCode()); + assertEquals("שלם, Dr. Livingstone!", response.body().asString()); + } + + @Test + void multiline() { + Response response = app.given().get("message/long/en"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone! \n How are you, Dr. Livingstone?", response.body().asString()); + } + + @Test + void defaultMessage() { + Response response = app.given().get("message/long/es"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone! \n How are you, Dr. Livingstone?", response.body().asString()); + } + + @Test + void multilineRTL() { + Response response = app.given().get("message/long/he"); + assertEquals(200, response.statusCode()); + String content = response.body().asString(); + assertEquals("שלם, Dr. Livingstone, מה נשמע?", content); + } + + @Test + void injected() { + Response response = app.given().get("message/"); + assertEquals(200, response.statusCode()); + String content = response.body().asString(); + assertEquals("שלם, אדם!", content); + } + +} diff --git a/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesReactiveIT.java b/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesReactiveIT.java new file mode 100644 index 0000000000..79bed2f217 --- /dev/null +++ b/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesReactiveIT.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.qute; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftMessageBundlesReactiveIT extends QuteReactiveIT { + +} diff --git a/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftQuteReactiveIT.java b/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftQuteReactiveIT.java new file mode 100644 index 0000000000..ff92486f78 --- /dev/null +++ b/qute/reactive/src/test/java/io/quarkus/ts/qute/OpenShiftQuteReactiveIT.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.qute; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftQuteReactiveIT extends QuteReactiveIT { + +} diff --git a/qute/reactive/src/test/java/io/quarkus/ts/qute/QuteReactiveIT.java b/qute/reactive/src/test/java/io/quarkus/ts/qute/QuteReactiveIT.java new file mode 100644 index 0000000000..cab782fcf1 --- /dev/null +++ b/qute/reactive/src/test/java/io/quarkus/ts/qute/QuteReactiveIT.java @@ -0,0 +1,255 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.ws.rs.core.MediaType; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.response.Response; + +@QuarkusScenario +public class QuteReactiveIT { + + private static final String UTF8_HTML = MediaType.TEXT_HTML + ";charset=UTF-8"; + + @QuarkusApplication + static RestService app = new RestService(); + + @Test + void smoke() { + Response response = app.given().get("/basic"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + + final String result = response.body().asString(); + assertEquals(59, result.length()); + assertEquals("\n

This page is rendered by Quarkus

\n\n", result); + } + + @Test + void location() { + Response response = app.given().get("/location"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertTrue(response.body().asString().contains("

This page is fetched and rendered by Quarkus

"), + response.body().asString()); + } + + @Test + void engine() { + Response response = app.given().get("/engine/basic"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertTrue(response.body().asString().contains("

This page is rendered by basic engine

"), + response.body().asString()); + } + + @Test + void register() { + Response check = app.given().get("/engine/another"); + assertEquals(HttpStatus.SC_NOT_FOUND, check.statusCode()); + + Response create = app.given().body("This page is rendered by {server} from HTTP request").put("/engine/another"); + assertEquals(HttpStatus.SC_CREATED, create.statusCode()); + + Response fetch = app.given().get("/engine/another"); + assertEquals(UTF8_HTML, fetch.contentType()); + assertEquals("This page is rendered by another engine from HTTP request", fetch.body().asString()); + + Response change = app.given().body("This page is rendered by {server} from HTTP request. Again") + .put("/engine/another"); + assertEquals(HttpStatus.SC_NO_CONTENT, change.statusCode()); + + Response fetchAgain = app.given().get("/engine/another"); + assertEquals("This page is rendered by another engine from HTTP request. Again", fetchAgain.body().asString()); + } + + @Test + void searchAndDelete() { + Response check = app.given().get("/engine/temporary"); + assertEquals(HttpStatus.SC_NOT_FOUND, check.statusCode()); + + Response create = app.given().body("This page is rendered by {server} from POST HTTP request") + .post("/engine/temporary"); + assertEquals(HttpStatus.SC_CREATED, create.statusCode()); + + final String same = "This page is rendered by temporary engine from POST HTTP request"; + + Response fetch = app.given().get("/engine/temporary"); + assertEquals(same, fetch.body().asString()); + + Response createAgain = app.given().body("This page is rendered by {server} from HTTP request. Again") + .post("/engine/temporary"); + assertEquals(HttpStatus.SC_CONFLICT, createAgain.statusCode()); + + Response fetchAgain = app.given().get("/engine/temporary"); + assertEquals(same, fetchAgain.body().asString()); + + assertEquals(HttpStatus.SC_OK, app.given().delete("/engine/temporary").statusCode()); + Response checkDeletion = app.given().get("/engine/temporary"); + assertEquals(HttpStatus.SC_NOT_FOUND, checkDeletion.statusCode()); + } + + @Test + void format() { + Response defaultType = app.given().get("/format?name=remote client"); + assertEquals(200, defaultType.statusCode()); + assertEquals(UTF8_HTML, defaultType.contentType()); + assertEquals("This page is rendered for \"remote client\" by Qute", defaultType.body().asString()); + + Response html = app.given().accept("text/html").get("/format?name=html client"); + assertEquals(200, html.statusCode()); + assertEquals(UTF8_HTML, html.contentType()); + assertEquals("This page is rendered for \"html client\" by Qute", html.body().asString()); + + Response plain = app.given().accept("text/plain").get("/format?name=plaintext client"); + assertEquals(200, plain.statusCode()); + assertEquals(MediaType.TEXT_PLAIN + ";charset=UTF-8", plain.contentType()); + assertEquals("This page is rendered for \"plaintext client\" by Qute", plain.body().asString()); + } + + @Test + void advancedFormat() { + Response response = app.given().get("/format-advanced?name=remote client"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertEquals("This text is fluently rendered for \"remote client\" by Qute", response.body().asString()); + } + + @Test + void expressions() { + Response response = app.given().get("/book"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + + String[] content = response.body().asString().replace("
", "").split("\n"); + assertEquals(20, content.length); + + assertEquals("", content[1]); + assertEquals("

This page is rendered by engine

", content[2]); + assertEquals("The book is called The Three Musketeers and is written by Alexandre Dumas", + content[3]); + + final int loopStartLine = 5; + assertEquals("It has 4 characters:", content[loopStartLine]); + assertEquals("1. d'Artagnan as well as", content[loopStartLine + 2].replace("'", "'")); + assertEquals("2. Athos and also", content[loopStartLine + 3]); + assertEquals("3. Porthos as well as", content[loopStartLine + 4]); + assertEquals("4. Aramis", content[loopStartLine + 5]); + + assertEquals( + "At first, d'Artagnan tries to fight on a duel with Athos, Porthos and Aramis, but later they become friends.", + content[12].replace("'", "'")); + final int arrayStartLine = 13; + assertEquals("Here are some numbers: 0 1 2 3 4", content[arrayStartLine]); + assertEquals("I have to say, that this is odd", content[arrayStartLine + 1]); + assertEquals("and this is even", content[arrayStartLine + 2]); + assertEquals("and this is odd", content[arrayStartLine + 3]); + assertEquals("and this is even", content[arrayStartLine + 4]); + assertEquals("and this is odd.", content[arrayStartLine + 5]); + } + + @Test + void encoding() { + Response response = app.given().get("/encoding"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + final String[] content = response.body().asString().split("\n"); + assertEquals("

Englishmen say hello

", content[2].stripLeading()); + assertEquals("

Češi říkají čau

", content[3].stripLeading()); + assertEquals("

Русские говорят привет

", content[4].stripLeading()); + assertEquals("

יהודים היגדים שלום

", content[5].stripLeading()); + assertEquals("{česky}", content[6].stripLeading()); + assertEquals("{English}", content[7].stripLeading()); + } + + @Test + void maps() { + Response response = app.given().get("/map"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + final String[] content = response.body().asString().split("\n"); + assertEquals("The capital of Tasmania is Hobart, Jakarta is a capital of Java and London is a capital of the UK.", + content[1].stripLeading()); + int keysLine = 6; + assertEquals("1. Java
", content[keysLine].stripLeading()); + assertEquals("2. Tasmania
", content[keysLine + 1].stripLeading()); + assertEquals("3. The Great Britain
", content[keysLine + 2].stripLeading()); + assertEquals("I am a Java programmer and there are 3 cities I know:
", content[9].stripLeading()); + int valuesLine = 10; + assertEquals("0. Jakarta
", content[valuesLine].stripLeading()); + assertEquals("1. Hobart
", content[valuesLine + 1].stripLeading()); + assertEquals("2. London
", content[valuesLine + 2].stripLeading()); + } + + @Test + void emptyMaps() { + Response response = app.given().get("/map?name=europe"); + assertEquals(200, response.statusCode()); + final String[] content = response.body().asString().split("\n"); + assertEquals("I am a Java programmer and there are zero cities I know:
", content[5].stripLeading()); + } + + @Test + void inheritance() { + Response base = app.given().get("/inheritance?name=base"); + assertEquals(200, base.statusCode()); + assertEquals(UTF8_HTML, base.contentType()); + final String[] baseContent = base.body().asString().split("\n"); + + assertEquals(7, baseContent.length); + assertEquals("A poem", baseContent[2].stripLeading()); + assertEquals("Empty body!", baseContent[4].stripLeading()); + + Response inherited = app.given().get("/inheritance?name=detail"); + assertEquals(200, inherited.statusCode()); + assertEquals(UTF8_HTML, inherited.contentType()); + + List updatedContent = Stream.of(inherited.body().asString()) + .map(string -> string.split("\n")) + .flatMap(Arrays::stream) + .filter(string -> !string.isEmpty()) + .filter(string -> !string.isEmpty()) + .collect(Collectors.toList()); + assertEquals(9, updatedContent.size()); + assertEquals("Auguries of Innocence", updatedContent.get(2).stripLeading()); + assertEquals("Every Morn and every Night
", updatedContent.get(4).stripLeading()); + assertEquals("Some are Born to sweet delight
", updatedContent.get(5).stripLeading()); + assertEquals("Some are Born to sweet delight
", updatedContent.get(6).stripLeading()); + assertEquals("Some are Born to Endless Night
", updatedContent.get(7).stripLeading()); + } + + @Test + void annotations() { + Response response = app.given().get("/annotated"); + assertEquals(200, response.statusCode()); + final String[] content = response.body().asString().split("\n"); + assertEquals("Joe slaps Seligman around a bit with a large trout
", content[0].stripLeading()); + assertEquals("This trout stays silent", content[1].stripLeading()); + assertEquals("A quick br()wn f()x jumps ()ver the lazy d()g", content[2].stripLeading()); + assertEquals("Airspeed velocity of an unladen swallow is 11 m/s", content[3].stripLeading()); + assertEquals("This random number was chosen by a roll of dice: 5", content[4].stripLeading()); + } + + @Test + void enums() { + Response dublin = app.given().get("/enums/dublin"); + assertEquals(200, dublin.statusCode()); + assertEquals("Good news, we will spend a week in Dublin!", dublin.body().asString()); + + Response bruges = app.given().get("/enums/bruges"); + assertEquals(200, bruges.statusCode()); + assertEquals("Good news, we will not spend a week in Bruges!", bruges.body().asString()); + } +} diff --git a/qute/synchronous/pom.xml b/qute/synchronous/pom.xml new file mode 100644 index 0000000000..b926425b3b --- /dev/null +++ b/qute/synchronous/pom.xml @@ -0,0 +1,19 @@ + + + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../../pom.xml + + 4.0.0 + qute-synchronous + jar + Quarkus QE TS: Synchronous Qute + + + io.quarkus + quarkus-resteasy-qute + + + diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/Application.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Application.java new file mode 100644 index 0000000000..96ec09f79f --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Application.java @@ -0,0 +1,224 @@ +package io.quarkus.ts.qute; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.inject.Inject; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.Location; +import io.quarkus.qute.Qute; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.i18n.Localized; + +@Path("") +public class Application { + + @Inject + Template basic; + + @Location("1.i18n.html") + Template multiLanguage; + + @Inject + Engine engine; + + @Localized("he") + Messages hebrew; + + @GET + @Path("/basic") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance base() { + return basic.data("server", "Quarkus"); + } + + @GET + @Path("/location") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance located() { + return multiLanguage.data("server", "Quarkus"); + } + + @GET + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response engine(@PathParam("name") String name) { + final Template template = engine.getTemplate(name); + if (template == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(template.data("server", name + " engine")).build(); + } + + @POST + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response registerNew(@PathParam("name") String name, String body) { + Response.ResponseBuilder result; + if (engine.getTemplate(name) == null) { + engine.putTemplate(name, engine.parse(body)); + result = Response.created(getUri("/engine/" + name)); + } else { + result = Response.status(Response.Status.CONFLICT); + } + return result.build(); + } + + @PUT + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response register(@PathParam("name") String name, String body) { + Template existing = engine.putTemplate(name, engine.parse(body)); + if (existing == null) { + return Response.created(getUri("/engine/" + name)).build(); + } else { + return Response.noContent().build(); + } + } + + @DELETE + @Path("/engine/{name}") + @Produces(MediaType.TEXT_HTML) + public Response delete(@PathParam("name") String name) { + engine.removeTemplates(templateName -> templateName.equals(name)); + return Response.ok().build(); + } + + @GET + @Path("/format") + @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) + public String format(@QueryParam("name") String name) { + return Qute.fmt("This page is rendered for \"{}\" by Qute", name); + } + + @GET + @Path("/format-advanced") + @Produces(MediaType.TEXT_HTML) + public String formatHtml(@QueryParam("name") String name) { + return Qute.fmt("This text is fluently rendered for \"{name}\" by Qute") + .data("name", name) + .render(); + } + + @GET + @Path("/book") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance book() { + Book musketeers = new Book("The Three Musketeers", "Alexandre Dumas", "d'Artagnan", "Athos", "Porthos", "Aramis"); + return engine.getTemplate("expressions") + .data("server", "engine") + .data("book", musketeers); + } + + @GET + @Path("/encoding") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance encoding() { + return engine.getTemplate("нелатынь") + .data("English", "hello") + .data("česky", "čau") + .data("по-русски", "привет") + .data("בעברית", "שלום"); + } + + @GET + @Path("/map") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance map(@QueryParam("name") @DefaultValue("islands") String region) { + Map map = new HashMap<>(); + if (region.equals("islands")) { + map.put("Tasmania", "Hobart"); + map.put("Java", "Jakarta"); + map.put("The Great Britain", "London"); + } + return engine + .getTemplate("maps") + .data("map", map); + } + + @GET + @Path("/inheritance") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance inheritance(@QueryParam("name") @DefaultValue("detail") String template) { + return engine.getTemplate(template).instance(); + } + + @GET + @Path("/annotated") + public String annotated() { + final Fish trout = new Fish("trout"); + return engine + .getTemplate("annotations") + .data("fish", trout) + .data("wrapper", new StringWrapper("A quick brown fox jumps over the lazy dog")) + .render(); + } + + @GET + @Path("/enums/{city}") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance enums(@PathParam("city") String city) { + final City destination = City.valueOf(city.toUpperCase()); + return engine.parse("Good news, we will{#if city == City:BRUGES} not{/if} spend a week in {city.naturalName}!") + .data("city", destination); + } + + @GET + @Path("/message/{locale}") + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance message(@PathParam("locale") String tag) { + return engine + .parse(String.format("{greeting:hello('%s')}", "Dr. Livingstone")) + .instance() + .setAttribute("locale", Locale.forLanguageTag(tag)); + } + + @GET + @Path("/message/long/{locale}") + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance longMessage(@PathParam("locale") String tag) { + return engine + .parse(String.format("{greeting:long_hello('%s')}", "Dr. Livingstone")) + .instance() + .setAttribute("locale", Locale.forLanguageTag(tag)); + } + + @GET + @Path("/message/") + @Produces(MediaType.TEXT_PLAIN) + public String injectedLocalization() { + return hebrew.hello("אדם"); + } + + private static URI getUri(String path) { + final Config system = ConfigProvider.getConfig(); + final String host = system.getValue("quarkus.http.host", String.class); + final Integer port = system.getValue("quarkus.http.port", Integer.class); + try { + return new URL("http", host, port, path).toURI(); + } catch (URISyntaxException | MalformedURLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/Book.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Book.java new file mode 100644 index 0000000000..cbf8df59d0 --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Book.java @@ -0,0 +1,16 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData +public class Book { + public final String title; + public final String author; + public final String[] characters; + + public Book(String title, String author, String... characters) { + this.title = title; + this.author = author; + this.characters = characters; + } +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/City.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/City.java new file mode 100644 index 0000000000..10495db47d --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/City.java @@ -0,0 +1,17 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum City { + BRUGES, + DUBLIN, + MOMBASA; + + public String naturalName() { + String full = this.name(); + String firstLetter = full.substring(0, 1); + String rest = full.substring(1).toLowerCase(); + return firstLetter + rest; + } +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/Fish.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Fish.java new file mode 100644 index 0000000000..0ce7edf1fa --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Fish.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData +public class Fish { + private final String name; + + public Fish(String name) { + this.name = name; + } + + public String saySomething() { + return String.format("This %s stays silent", this.name); + } + + public String slap(String user, String target) { + return String.format("%s slaps %s around a bit with a large %s", user, target, this.name); + } +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/Messages.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Messages.java new file mode 100644 index 0000000000..c47ca810dc --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Messages.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; + +@MessageBundle("greeting") +public interface Messages { + @Message("Hello, {name}!") + String hello(String name); + + @Message("Hello, {name}! \n How are you, {name}?") + String long_hello(String name); + +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/SpanishMessages.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/SpanishMessages.java new file mode 100644 index 0000000000..002d919165 --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/SpanishMessages.java @@ -0,0 +1,12 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.i18n.Localized; +import io.quarkus.qute.i18n.Message; + +@Localized("es") +public interface SpanishMessages extends Messages { + + @Override + @Message("Hola, {name}!") + String hello(String name); +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/StringWrapper.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/StringWrapper.java new file mode 100644 index 0000000000..641cc5b74f --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/StringWrapper.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateData; + +@TemplateData(target = String.class) +@TemplateData //required for native +public class StringWrapper { + public final String content; + + public StringWrapper(String content) { + this.content = content; + } +} diff --git a/qute/synchronous/src/main/java/io/quarkus/ts/qute/Variables.java b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Variables.java new file mode 100644 index 0000000000..3776afb106 --- /dev/null +++ b/qute/synchronous/src/main/java/io/quarkus/ts/qute/Variables.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.qute; + +import io.quarkus.qute.TemplateGlobal; + +@TemplateGlobal +public class Variables { + static final int airspeedVelocityOfAnUnladenSwallow = 11; + + @TemplateGlobal(name = "random") + static int getRandomNumber() { + return 5; + } +} diff --git a/qute/synchronous/src/main/resources/messages/greeting_cs.properties b/qute/synchronous/src/main/resources/messages/greeting_cs.properties new file mode 100644 index 0000000000..21b0dd4383 --- /dev/null +++ b/qute/synchronous/src/main/resources/messages/greeting_cs.properties @@ -0,0 +1 @@ +hello=Ahoj, {name}! diff --git a/qute/synchronous/src/main/resources/messages/greeting_he.properties b/qute/synchronous/src/main/resources/messages/greeting_he.properties new file mode 100644 index 0000000000..270f414488 --- /dev/null +++ b/qute/synchronous/src/main/resources/messages/greeting_he.properties @@ -0,0 +1,3 @@ +hello=שלם, {name}! +long_hello=שלם, {name}, \ + מה נשמע? diff --git a/qute/synchronous/src/main/resources/templates/1.i18n.html b/qute/synchronous/src/main/resources/templates/1.i18n.html new file mode 100644 index 0000000000..1ee7cb132e --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/1.i18n.html @@ -0,0 +1,3 @@ + +

This page is fetched and rendered by {server}

+ diff --git a/qute/synchronous/src/main/resources/templates/annotations.html b/qute/synchronous/src/main/resources/templates/annotations.html new file mode 100644 index 0000000000..6877d0805b --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/annotations.html @@ -0,0 +1,5 @@ +{fish.slap("Joe", "Seligman")}
+{fish.saySomething} +{wrapper.content.replace("o","()")} +Airspeed velocity of an unladen swallow is {airspeedVelocityOfAnUnladenSwallow} m/s +This random number was chosen by a roll of dice: {random} diff --git a/qute/synchronous/src/main/resources/templates/base.html b/qute/synchronous/src/main/resources/templates/base.html new file mode 100644 index 0000000000..b50383829a --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/base.html @@ -0,0 +1,10 @@ + + +{#insert title}A poem{/} + +{#insert first}{/} +{#insert}{/} +{#insert}Empty body!{/} +{#insert last}{/} + + diff --git a/qute/synchronous/src/main/resources/templates/basic.html b/qute/synchronous/src/main/resources/templates/basic.html new file mode 100644 index 0000000000..a4a04a3305 --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/basic.html @@ -0,0 +1,3 @@ + +

This page is rendered by {server}

+ diff --git a/qute/synchronous/src/main/resources/templates/detail.html b/qute/synchronous/src/main/resources/templates/detail.html new file mode 100644 index 0000000000..853386b025 --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/detail.html @@ -0,0 +1,6 @@ +{#include base} +{#title}Auguries of Innocence{/title} +{#first}Every Morn and every Night
{/first} +{#last}Some are Born to Endless Night
{/last} +Some are Born to sweet delight
+{/include} diff --git a/qute/synchronous/src/main/resources/templates/expressions.html b/qute/synchronous/src/main/resources/templates/expressions.html new file mode 100644 index 0000000000..eaef33c4e3 --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/expressions.html @@ -0,0 +1,17 @@ + + +

This page is rendered by {server}

+The book is called {book.title} and is written by {book.['author']} +
+It has {book.characters.length} characters: +
+{#for character in book.characters} +{character_count}. {character}{#if character_hasNext} {#if character_odd }as well as{/if}{#if character_even }and also{/if}{/if}
+{/for} +
+At first, {book.characters.take(1).0} tries to fight on a duel with {book.characters.takeLast(3)[0]}, {book.characters.takeLast(3).get(1)} and {book.characters.takeLast(3).2}, but later they become friends.
+Here are some numbers:{#for i in 5} {i_index}{/for}
+{#for i in 5} +{#if i_isFirst}I have to say, that{#else}and{/if} this is {i_indexParity}{#if i_isLast}.{/if}
+{/for} + diff --git a/qute/synchronous/src/main/resources/templates/maps.html b/qute/synchronous/src/main/resources/templates/maps.html new file mode 100644 index 0000000000..c597d533f7 --- /dev/null +++ b/qute/synchronous/src/main/resources/templates/maps.html @@ -0,0 +1,16 @@ + +{#if !map.isEmpty} +The capital of Tasmania is {map.Tasmania}, {map.['Java']} is a capital of Java and {map.get('The Great Britain')} is a capital of the UK. +{/if} + +
+{#for country in map.keySet} + {country_count}. {country}
+{/for} +I am a Java programmer and there are {#if map.isEmpty}zero{#else}{map.size}{/if} cities I know:
+{#for capital in map.values} + {capital_index}. {capital}
+{/for} + diff --git "a/qute/synchronous/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" "b/qute/synchronous/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" new file mode 100644 index 0000000000..0a0f2024b7 --- /dev/null +++ "b/qute/synchronous/src/main/resources/templates/\320\275\320\265\320\273\320\260\321\202\321\213\320\275\321\214.html" @@ -0,0 +1,9 @@ + + +

Englishmen say {English}

+

Češi říkají {česky}

+

Русские говорят {по-русски}

+

יהודים היגדים {בעברית}

+{|{česky} +{English}|} + diff --git a/qute/synchronous/src/test/java/io/quarkus/ts/qute/DevModeQuteIT.java b/qute/synchronous/src/test/java/io/quarkus/ts/qute/DevModeQuteIT.java new file mode 100644 index 0000000000..8b767fb4f9 --- /dev/null +++ b/qute/synchronous/src/test/java/io/quarkus/ts/qute/DevModeQuteIT.java @@ -0,0 +1,48 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.DevModeQuarkusService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.DevModeQuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; +import io.restassured.response.Response; + +@QuarkusScenario +public class DevModeQuteIT { + private static final String FILE = "src/main/resources/templates/basic.html"; + private static final String RESERVE = "src/main/resources/templates/basic.html.bck"; + + @DevModeQuarkusApplication + static DevModeQuarkusService app = new DevModeQuarkusService(); + + @BeforeAll + static void beforeAll() { + app.copyFile(FILE, RESERVE); + } + + @Test + void changeTemplate() { + Response was = app.given().get("/basic"); + assertEquals("\n

This page is rendered by Quarkus

\n\n", was.body().asString()); + app.modifyFile(FILE, string -> string.replace("This page is rendered", "This page is being edited")); + AwaitilityUtils.untilAsserted(() -> { + Response changed = app.given().get("/basic"); + assertEquals("\n

This page is being edited by Quarkus

\n\n", changed.body().asString()); + }); + app.logs().assertContains("File change detected"); + app.logs().assertContains("Restarting quarkus due to changes in basic.html"); + + assertEquals("\n

This page is being edited by basic engine

\n\n", + app.given().get("/engine/basic").body().asString()); + } + + @AfterAll + static void afterAll() { + app.copyFile(RESERVE, FILE); + } +} diff --git a/qute/synchronous/src/test/java/io/quarkus/ts/qute/MessageBundlesIT.java b/qute/synchronous/src/test/java/io/quarkus/ts/qute/MessageBundlesIT.java new file mode 100644 index 0000000000..b968bcdcc7 --- /dev/null +++ b/qute/synchronous/src/test/java/io/quarkus/ts/qute/MessageBundlesIT.java @@ -0,0 +1,75 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.response.Response; + +@QuarkusScenario +public class MessageBundlesIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Test + void smoke() { + Response response = app.given().get("message/en"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone!", response.body().asString()); + } + + @Test + void fromClass() { + Response response = app.given().get("message/es"); + assertEquals(200, response.statusCode()); + assertEquals("Hola, Dr. Livingstone!", response.body().asString()); + } + + @Test + void fromFile() { + Response response = app.given().get("message/cs"); + assertEquals(200, response.statusCode()); + assertEquals("Ahoj, Dr. Livingstone!", response.body().asString()); + } + + @Test + void rtl() { + Response response = app.given().get("message/he"); + assertEquals(200, response.statusCode()); + assertEquals("שלם, Dr. Livingstone!", response.body().asString()); + } + + @Test + void multiline() { + Response response = app.given().get("message/long/en"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone! \n How are you, Dr. Livingstone?", response.body().asString()); + } + + @Test + void defaultMessage() { + Response response = app.given().get("message/long/es"); + assertEquals(200, response.statusCode()); + assertEquals("Hello, Dr. Livingstone! \n How are you, Dr. Livingstone?", response.body().asString()); + } + + @Test + void multilineRTL() { + Response response = app.given().get("message/long/he"); + assertEquals(200, response.statusCode()); + String content = response.body().asString(); + assertEquals("שלם, Dr. Livingstone, מה נשמע?", content); + } + + @Test + void injected() { + Response response = app.given().get("message/"); + assertEquals(200, response.statusCode()); + String content = response.body().asString(); + assertEquals("שלם, אדם!", content); + } + +} diff --git a/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesIT.java b/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesIT.java new file mode 100644 index 0000000000..f7e28d8aa5 --- /dev/null +++ b/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftMessageBundlesIT.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.qute; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftMessageBundlesIT extends QuteIT { + +} diff --git a/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftQuteIT.java b/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftQuteIT.java new file mode 100644 index 0000000000..a32523063f --- /dev/null +++ b/qute/synchronous/src/test/java/io/quarkus/ts/qute/OpenShiftQuteIT.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.qute; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftQuteIT extends QuteIT { + +} diff --git a/qute/synchronous/src/test/java/io/quarkus/ts/qute/QuteIT.java b/qute/synchronous/src/test/java/io/quarkus/ts/qute/QuteIT.java new file mode 100644 index 0000000000..c92aea3dd3 --- /dev/null +++ b/qute/synchronous/src/test/java/io/quarkus/ts/qute/QuteIT.java @@ -0,0 +1,255 @@ +package io.quarkus.ts.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.ws.rs.core.MediaType; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.response.Response; + +@QuarkusScenario +public class QuteIT { + + private static final String UTF8_HTML = MediaType.TEXT_HTML + ";charset=UTF-8"; + + @QuarkusApplication + static RestService app = new RestService(); + + @Test + void smoke() { + Response response = app.given().get("/basic"); + assertEquals(200, response.statusCode()); + assertEquals(MediaType.TEXT_HTML, response.contentType()); //https://github.com/quarkusio/quarkus/issues/27872 + + final String result = response.body().asString(); + assertEquals(59, result.length()); + assertEquals("\n

This page is rendered by Quarkus

\n\n", result); + } + + @Test + void location() { + Response response = app.given().get("/location"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertTrue(response.body().asString().contains("

This page is fetched and rendered by Quarkus

"), + response.body().asString()); + } + + @Test + void engine() { + Response response = app.given().get("/engine/basic"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertTrue(response.body().asString().contains("

This page is rendered by basic engine

"), + response.body().asString()); + } + + @Test + void register() { + Response check = app.given().get("/engine/another"); + assertEquals(HttpStatus.SC_NOT_FOUND, check.statusCode()); + + Response create = app.given().body("This page is rendered by {server} from HTTP request").put("/engine/another"); + assertEquals(HttpStatus.SC_CREATED, create.statusCode()); + + Response fetch = app.given().get("/engine/another"); + assertEquals(UTF8_HTML, fetch.contentType()); + assertEquals("This page is rendered by another engine from HTTP request", fetch.body().asString()); + + Response change = app.given().body("This page is rendered by {server} from HTTP request. Again") + .put("/engine/another"); + assertEquals(HttpStatus.SC_NO_CONTENT, change.statusCode()); + + Response fetchAgain = app.given().get("/engine/another"); + assertEquals("This page is rendered by another engine from HTTP request. Again", fetchAgain.body().asString()); + } + + @Test + void searchAndDelete() { + Response check = app.given().get("/engine/temporary"); + assertEquals(HttpStatus.SC_NOT_FOUND, check.statusCode()); + + Response create = app.given().body("This page is rendered by {server} from POST HTTP request") + .post("/engine/temporary"); + assertEquals(HttpStatus.SC_CREATED, create.statusCode()); + + final String same = "This page is rendered by temporary engine from POST HTTP request"; + + Response fetch = app.given().get("/engine/temporary"); + assertEquals(same, fetch.body().asString()); + + Response createAgain = app.given().body("This page is rendered by {server} from HTTP request. Again") + .post("/engine/temporary"); + assertEquals(HttpStatus.SC_CONFLICT, createAgain.statusCode()); + + Response fetchAgain = app.given().get("/engine/temporary"); + assertEquals(same, fetchAgain.body().asString()); + + assertEquals(HttpStatus.SC_OK, app.given().delete("/engine/temporary").statusCode()); + Response checkDeletion = app.given().get("/engine/temporary"); + assertEquals(HttpStatus.SC_NOT_FOUND, checkDeletion.statusCode()); + } + + @Test + void format() { + Response defaultType = app.given().get("/format?name=remote client"); + assertEquals(200, defaultType.statusCode()); + assertEquals(UTF8_HTML, defaultType.contentType()); + assertEquals("This page is rendered for \"remote client\" by Qute", defaultType.body().asString()); + + Response html = app.given().accept("text/html").get("/format?name=html client"); + assertEquals(200, html.statusCode()); + assertEquals(UTF8_HTML, html.contentType()); + assertEquals("This page is rendered for \"html client\" by Qute", html.body().asString()); + + Response plain = app.given().accept("text/plain").get("/format?name=plaintext client"); + assertEquals(200, plain.statusCode()); + assertEquals(MediaType.TEXT_PLAIN + ";charset=UTF-8", plain.contentType()); + assertEquals("This page is rendered for \"plaintext client\" by Qute", plain.body().asString()); + } + + @Test + void advancedFormat() { + Response response = app.given().get("/format-advanced?name=remote client"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + assertEquals("This text is fluently rendered for \"remote client\" by Qute", response.body().asString()); + } + + @Test + void expressions() { + Response response = app.given().get("/book"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + + String[] content = response.body().asString().replace("
", "").split("\n"); + assertEquals(20, content.length); + + assertEquals("", content[1]); + assertEquals("

This page is rendered by engine

", content[2]); + assertEquals("The book is called The Three Musketeers and is written by Alexandre Dumas", + content[3]); + + final int loopStartLine = 5; + assertEquals("It has 4 characters:", content[loopStartLine]); + assertEquals("1. d'Artagnan as well as", content[loopStartLine + 2].replace("'", "'")); + assertEquals("2. Athos and also", content[loopStartLine + 3]); + assertEquals("3. Porthos as well as", content[loopStartLine + 4]); + assertEquals("4. Aramis", content[loopStartLine + 5]); + + assertEquals( + "At first, d'Artagnan tries to fight on a duel with Athos, Porthos and Aramis, but later they become friends.", + content[12].replace("'", "'")); + final int arrayStartLine = 13; + assertEquals("Here are some numbers: 0 1 2 3 4", content[arrayStartLine]); + assertEquals("I have to say, that this is odd", content[arrayStartLine + 1]); + assertEquals("and this is even", content[arrayStartLine + 2]); + assertEquals("and this is odd", content[arrayStartLine + 3]); + assertEquals("and this is even", content[arrayStartLine + 4]); + assertEquals("and this is odd.", content[arrayStartLine + 5]); + } + + @Test + void encoding() { + Response response = app.given().get("/encoding"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + final String[] content = response.body().asString().split("\n"); + assertEquals("

Englishmen say hello

", content[2].stripLeading()); + assertEquals("

Češi říkají čau

", content[3].stripLeading()); + assertEquals("

Русские говорят привет

", content[4].stripLeading()); + assertEquals("

יהודים היגדים שלום

", content[5].stripLeading()); + assertEquals("{česky}", content[6].stripLeading()); + assertEquals("{English}", content[7].stripLeading()); + } + + @Test + void maps() { + Response response = app.given().get("/map"); + assertEquals(200, response.statusCode()); + assertEquals(UTF8_HTML, response.contentType()); + final String[] content = response.body().asString().split("\n"); + assertEquals("The capital of Tasmania is Hobart, Jakarta is a capital of Java and London is a capital of the UK.", + content[1].stripLeading()); + int keysLine = 6; + assertEquals("1. Java
", content[keysLine].stripLeading()); + assertEquals("2. Tasmania
", content[keysLine + 1].stripLeading()); + assertEquals("3. The Great Britain
", content[keysLine + 2].stripLeading()); + assertEquals("I am a Java programmer and there are 3 cities I know:
", content[9].stripLeading()); + int valuesLine = 10; + assertEquals("0. Jakarta
", content[valuesLine].stripLeading()); + assertEquals("1. Hobart
", content[valuesLine + 1].stripLeading()); + assertEquals("2. London
", content[valuesLine + 2].stripLeading()); + } + + @Test + void emptyMaps() { + Response response = app.given().get("/map?name=europe"); + assertEquals(200, response.statusCode()); + final String[] content = response.body().asString().split("\n"); + assertEquals("I am a Java programmer and there are zero cities I know:
", content[5].stripLeading()); + } + + @Test + void inheritance() { + Response base = app.given().get("/inheritance?name=base"); + assertEquals(200, base.statusCode()); + assertEquals(UTF8_HTML, base.contentType()); + final String[] baseContent = base.body().asString().split("\n"); + + assertEquals(7, baseContent.length); + assertEquals("A poem", baseContent[2].stripLeading()); + assertEquals("Empty body!", baseContent[4].stripLeading()); + + Response inherited = app.given().get("/inheritance?name=detail"); + assertEquals(200, inherited.statusCode()); + assertEquals(UTF8_HTML, inherited.contentType()); + + List updatedContent = Stream.of(inherited.body().asString()) + .map(string -> string.split("\n")) + .flatMap(Arrays::stream) + .filter(string -> !string.isEmpty()) + .filter(string -> !string.isEmpty()) + .collect(Collectors.toList()); + assertEquals(9, updatedContent.size()); + assertEquals("Auguries of Innocence", updatedContent.get(2).stripLeading()); + assertEquals("Every Morn and every Night
", updatedContent.get(4).stripLeading()); + assertEquals("Some are Born to sweet delight
", updatedContent.get(5).stripLeading()); + assertEquals("Some are Born to sweet delight
", updatedContent.get(6).stripLeading()); + assertEquals("Some are Born to Endless Night
", updatedContent.get(7).stripLeading()); + } + + @Test + void annotations() { + Response response = app.given().get("/annotated"); + assertEquals(200, response.statusCode()); + final String[] content = response.body().asString().split("\n"); + assertEquals("Joe slaps Seligman around a bit with a large trout
", content[0].stripLeading()); + assertEquals("This trout stays silent", content[1].stripLeading()); + assertEquals("A quick br()wn f()x jumps ()ver the lazy d()g", content[2].stripLeading()); + assertEquals("Airspeed velocity of an unladen swallow is 11 m/s", content[3].stripLeading()); + assertEquals("This random number was chosen by a roll of dice: 5", content[4].stripLeading()); + } + + @Test + void enums() { + Response dublin = app.given().get("/enums/dublin"); + assertEquals(200, dublin.statusCode()); + assertEquals("Good news, we will spend a week in Dublin!", dublin.body().asString()); + + Response bruges = app.given().get("/enums/bruges"); + assertEquals(200, bruges.statusCode()); + assertEquals("Good news, we will not spend a week in Bruges!", bruges.body().asString()); + } +}